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

MRG: Revamp HTML reprs of Raw, Epochs, Evoked, and Info #12583

Merged
merged 111 commits into from
May 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
111 commits
Select commit Hold shift + click to select a range
4ec09fa
Update `Evoked` HTML repr to closer match the one from `Epochs`, both…
hoechenberger Apr 27, 2024
d722328
[autofix.ci] apply automated fixes
autofix-ci[bot] Apr 27, 2024
a239de3
Fix
hoechenberger Apr 27, 2024
1a5f612
Merge branch 'evoked-html-repr' of https://github.com/hoechenberger/m…
hoechenberger Apr 27, 2024
f292ab8
Rework Raw, Epochs, Evoked HTML repr
hoechenberger Apr 28, 2024
8bebdf8
Update changelog
hoechenberger Apr 28, 2024
7f86966
Include measurement date and participant
hoechenberger Apr 28, 2024
16ffb44
Fix tests
hoechenberger Apr 28, 2024
49438dc
Rename digpoint row
hoechenberger Apr 28, 2024
2f61ab4
Updates for Info and Raw
hoechenberger Apr 29, 2024
d6f23c9
Fix test
hoechenberger Apr 29, 2024
af2659a
Drive-by fix for an unrelated test bug
hoechenberger Apr 29, 2024
9d01328
Time is UTC
hoechenberger Apr 29, 2024
da0e667
Update 12583.newfeature.rst
hoechenberger Apr 29, 2024
273dc10
Touch tutorial [circle full]
hoechenberger Apr 30, 2024
158a7b6
Merge branch 'main' into evoked-html-repr
hoechenberger Apr 30, 2024
4dae7b1
Better [circle full]
hoechenberger Apr 30, 2024
9da01a6
Merge branch 'evoked-html-repr' of https://github.com/hoechenberger/m…
hoechenberger Apr 30, 2024
d654b5b
Better vertical alignment [circle full]
hoechenberger Apr 30, 2024
5845b21
Fix test [circle full]
hoechenberger Apr 30, 2024
9432791
Add transition [circle full]
hoechenberger Apr 30, 2024
6162214
Better [circle full]
hoechenberger Apr 30, 2024
289561e
Merge branch 'main' of https://github.com/mne-tools/mne-python into e…
hoechenberger Apr 30, 2024
caaf0e7
Improve table layout
hoechenberger Apr 30, 2024
56770e0
Formatting
hoechenberger Apr 30, 2024
9c0cbc1
Add epochs
hoechenberger Apr 30, 2024
5d7aa12
WIP
hoechenberger May 1, 2024
4474605
Merge branch 'main' into evoked-html-repr
hoechenberger May 1, 2024
0d73dce
Merge branch 'main' into evoked-html-repr
hoechenberger May 1, 2024
72284c9
Raw and Info
hoechenberger May 1, 2024
63c04d4
Remove macros
hoechenberger May 1, 2024
56cb730
Merge branch 'main' into evoked-html-repr
hoechenberger May 2, 2024
2beaf0a
Reorder and fix
hoechenberger May 2, 2024
7dadcd5
Fix filters button
hoechenberger May 2, 2024
2fafbd5
Epochs WIP
hoechenberger May 2, 2024
cc75a45
Fix data type display
hoechenberger May 2, 2024
aba2679
Clean up
hoechenberger May 2, 2024
58716c2
Rename
hoechenberger May 2, 2024
3541d52
Epochs
hoechenberger May 2, 2024
fc74403
Evoked WIP
hoechenberger May 2, 2024
aa8da55
Merge branch 'main' into evoked-html-repr
hoechenberger May 2, 2024
96429b4
Consolidate and finalize Evoked
hoechenberger May 3, 2024
fcd5e8e
Merge branch 'main' of https://github.com/mne-tools/mne-python into e…
hoechenberger May 3, 2024
2db6499
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 3, 2024
ee427be
Avoid circular imports
hoechenberger May 3, 2024
fc0ecaa
Fix
hoechenberger May 3, 2024
5e1f63f
More circular import woes
hoechenberger May 3, 2024
2e13ac0
Give up
hoechenberger May 3, 2024
cbf29ee
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] May 3, 2024
0c873c6
Document API change [circle full]
hoechenberger May 3, 2024
68d6396
[autofix.ci] apply automated fixes
autofix-ci[bot] May 3, 2024
08743aa
[autofix.ci] apply automated fixes (attempt 2/3)
autofix-ci[bot] May 3, 2024
739ba66
Fix reference [circle full]
hoechenberger May 3, 2024
d7ebed3
Merge branch 'evoked-html-repr' of https://github.com/hoechenberger/m…
hoechenberger May 3, 2024
dbac896
Go the easy route [circle full]
hoechenberger May 3, 2024
b37b430
[autofix.ci] apply automated fixes
autofix-ci[bot] May 3, 2024
16446e4
Touch tutorial [circle full]
hoechenberger May 3, 2024
79c385d
Merge branch 'evoked-html-repr' of https://github.com/hoechenberger/m…
hoechenberger May 3, 2024
b1c41db
Again [circle full]
hoechenberger May 3, 2024
d269ed1
Fix forward
hoechenberger May 3, 2024
632fcd6
Touch [circle full]
hoechenberger May 3, 2024
d2268d5
Fix time range
hoechenberger May 3, 2024
e060e9c
Cleaner
hoechenberger May 3, 2024
a0503ee
Fix experimenter
hoechenberger May 3, 2024
b819e73
Handle doc build [circle full]
hoechenberger May 3, 2024
cfadd4e
Forgot one [circle full]
hoechenberger May 3, 2024
1cf1698
Modify changelog
hoechenberger May 4, 2024
e3ca777
Replace plus / minus button content with right / down carets
hoechenberger May 4, 2024
01bfe69
Update repr.css
hoechenberger May 4, 2024
be894eb
Improve striping of section headers
hoechenberger May 4, 2024
54c526f
Merge branch 'evoked-html-repr' of https://github.com/hoechenberger/m…
hoechenberger May 4, 2024
d2e1fce
Far from perfect, but already better
hoechenberger May 6, 2024
0f6281d
Merge branch 'main' into evoked-html-repr
hoechenberger May 6, 2024
3b02526
Smaller caret (but still too much padding)
hoechenberger May 6, 2024
db59108
Merge branch 'evoked-html-repr' of https://github.com/hoechenberger/m…
hoechenberger May 6, 2024
0b6f295
Probably better
hoechenberger May 6, 2024
49bd120
Don't collapse by default!
hoechenberger May 6, 2024
d943d47
Apply suggestions from code review
hoechenberger May 7, 2024
37776c9
Update mne/html_templates/_templates.py
hoechenberger May 7, 2024
74870e7
Fix date string
hoechenberger May 7, 2024
fd976a5
Change dt string to "YYYY-MM-DD at hh:mm:ss UTC"
hoechenberger May 8, 2024
aacb2c4
Data type -> MNE object type
hoechenberger May 8, 2024
71fd43f
Remove "channels" from Good, Bad, EOG, ECG, as this is redundant with…
hoechenberger May 8, 2024
4c1ab52
Fix tests
hoechenberger May 8, 2024
32cae9b
Data -> Acquisition
hoechenberger May 8, 2024
c864a5e
Create random UUIDs
hoechenberger May 14, 2024
d63674b
Rework channel names
hoechenberger May 15, 2024
523dccb
Merge branch 'main' of https://github.com/mne-tools/mne-python into e…
hoechenberger May 15, 2024
c0871c8
Fix test
hoechenberger May 15, 2024
123e3cb
Fix tests
hoechenberger May 15, 2024
70f13f4
Better
hoechenberger May 15, 2024
0712294
Merge branch 'main' into evoked-html-repr
hoechenberger May 15, 2024
4554e6c
Fix Forward repr
hoechenberger May 15, 2024
ef7655b
Update scraper
hoechenberger May 16, 2024
1d40199
Work around layout issue in scraper
hoechenberger May 16, 2024
6a04db2
Add text color
hoechenberger May 16, 2024
fa3f91c
Fix test
hoechenberger May 17, 2024
bd7ca5a
Merge branch 'main' of https://github.com/mne-tools/mne-python into e…
hoechenberger May 17, 2024
0d6cba0
Update tutorial
hoechenberger May 17, 2024
f6c3f4e
STCs are an exception
hoechenberger May 17, 2024
d02e905
Undo
hoechenberger May 17, 2024
9e7225b
Move table
hoechenberger May 17, 2024
2bf2a50
Typo
hoechenberger May 17, 2024
386e6a5
Better stripes
hoechenberger May 17, 2024
e997a28
Merge branch 'main' into evoked-html-repr
hoechenberger May 18, 2024
9bce870
Remove background in first column
hoechenberger May 18, 2024
5ee55fa
Support for dark mode
hoechenberger May 18, 2024
6df9b2d
Make entire row clickable
hoechenberger May 18, 2024
1acd683
Fix alignment for Jupyter
hoechenberger May 19, 2024
b8b8fd6
More Jupyter Lab fixes
hoechenberger May 19, 2024
ef52e37
Merge branch 'main' into evoked-html-repr
hoechenberger May 21, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions doc/changes/devel/12583.apichange.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
``mne.Info.ch_names`` will now return an empty list instead of raising a ``KeyError`` if no channels
are present, by `Richard Höchenberger`_.
4 changes: 4 additions & 0 deletions doc/changes/devel/12583.newfeature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
The HTML representations of :class:`~mne.io.Raw`, :class:`~mne.Epochs`,
and :class:`~mne.Evoked` (which you will see e.g. when working with Jupyter Notebooks or
:class:`~mne.Report`) have been updated to be more consistent and contain
slightly more information, by `Richard Höchenberger`_.
82 changes: 8 additions & 74 deletions mne/_fiff/meas_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import datetime
import operator
import string
from collections import Counter, OrderedDict, defaultdict
from collections import Counter, OrderedDict
from collections.abc import Mapping
from copy import deepcopy
from io import BytesIO
Expand Down Expand Up @@ -1838,84 +1838,18 @@ def _update_redundant(self):

@property
def ch_names(self):
return self["ch_names"]

def _get_chs_for_repr(self):
titles = _handle_default("titles")

# good channels
good_names = defaultdict(lambda: list())
for ci, ch_name in enumerate(self["ch_names"]):
if ch_name in self["bads"]:
continue
ch_type = channel_type(self, ci)
good_names[ch_type].append(ch_name)
good_channels = ", ".join(
[f"{len(v)} {titles.get(k, k.upper())}" for k, v in good_names.items()]
)
for key in ("ecg", "eog"): # ensure these are present
if key not in good_names:
good_names[key] = list()
for key, val in good_names.items():
good_names[key] = ", ".join(val) or "Not available"

# bad channels
bad_channels = ", ".join(self["bads"]) or "None"
try:
ch_names = self["ch_names"]
except KeyError:
ch_names = []

return good_channels, bad_channels, good_names["ecg"], good_names["eog"]
return ch_names

@repr_html
def _repr_html_(self, caption=None, duration=None, filenames=None):
def _repr_html_(self):
"""Summarize info for HTML representation."""
if isinstance(caption, str):
html = f"<h4>{caption}</h4>"
else:
html = ""

good_channels, bad_channels, ecg, eog = self._get_chs_for_repr()

# TODO
# Most of the following checks are to ensure that we get a proper repr
# for Forward['info'] (and probably others like
# InverseOperator['info']??), which doesn't seem to follow our standard
# Info structure used elsewhere.
# Proposed solution for a future refactoring:
# Forward['info'] should get its own Info subclass (with respective
# repr).

# meas date
meas_date = self.get("meas_date")
if meas_date is not None:
meas_date = meas_date.strftime("%B %d, %Y %H:%M:%S") + " GMT"

projs = self.get("projs")
if projs:
projs = [
f'{p["desc"]} : {"on" if p["active"] else "off"}' for p in self["projs"]
]
else:
projs = None

info_template = _get_html_template("repr", "info.html.jinja")
sections = ("General", "Channels", "Data")
return html + info_template.render(
sections=sections,
caption=caption,
meas_date=meas_date,
projs=projs,
ecg=ecg,
eog=eog,
good_channels=good_channels,
bad_channels=bad_channels,
dig=self.get("dig"),
subject_info=self.get("subject_info"),
lowpass=self.get("lowpass"),
highpass=self.get("highpass"),
sfreq=self.get("sfreq"),
experimenter=self.get("experimenter"),
duration=duration,
filenames=filenames,
)
return info_template.render(info=self)

def save(self, fname):
"""Write measurement info in fif file.
Expand Down
15 changes: 7 additions & 8 deletions mne/_fiff/tests/test_meas_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -896,18 +896,17 @@ def test_repr_html():
info["projs"] = []
assert "Projections" not in info._repr_html_()
info["bads"] = []
assert "None" in info._repr_html_()
assert "bad" not in info._repr_html_()
info["bads"] = ["MEG 2443", "EEG 053"]
assert "MEG 2443" in info._repr_html_()
assert "EEG 053" in info._repr_html_()
assert "1 bad" in info._repr_html_() # 1 for each channel type

html = info._repr_html_()
for ch in [ # good channel counts
"203 Gradiometers",
"102 Magnetometers",
"9 Stimulus",
"59 EEG",
"1 EOG",
"203", # grad
"102", # mag
"9", # stim
"59", # eeg
"1", # eog
]:
assert ch in html

Expand Down
17 changes: 10 additions & 7 deletions mne/epochs.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from copy import deepcopy
from functools import partial
from inspect import getfullargspec
from pathlib import Path

import numpy as np
from scipy.interpolate import interp1d
Expand Down Expand Up @@ -2062,12 +2063,6 @@ def __repr__(self):

@repr_html
def _repr_html_(self):
if self.baseline is None:
baseline = "off"
else:
baseline = tuple([f"{b:.3f}" for b in self.baseline])
baseline = f"{baseline[0]} – {baseline[1]} s"

if isinstance(self.event_id, dict):
event_strings = []
for k, v in sorted(self.event_id.items()):
Expand All @@ -2085,7 +2080,15 @@ def _repr_html_(self):
event_strings = None

t = _get_html_template("repr", "epochs.html.jinja")
t = t.render(epochs=self, baseline=baseline, events=event_strings)
t = t.render(
inst=self,
filenames=(
[Path(self.filename).name]
if getattr(self, "filename", None) is not None
else None
),
event_counts=event_strings,
)
return t

@verbose
Expand Down
16 changes: 9 additions & 7 deletions mne/evoked.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

from copy import deepcopy
from inspect import getfullargspec
from pathlib import Path
from typing import Union

import numpy as np
Expand Down Expand Up @@ -467,14 +468,15 @@ def __repr__(self): # noqa: D105

@repr_html
def _repr_html_(self):
if self.baseline is None:
baseline = "off"
else:
baseline = tuple([f"{b:.3f}" for b in self.baseline])
baseline = f"{baseline[0]} – {baseline[1]} s"

t = _get_html_template("repr", "evoked.html.jinja")
t = t.render(evoked=self, baseline=baseline)
t = t.render(
inst=self,
filenames=(
[Path(self.filename).name]
if getattr(self, "filename", None) is not None
else None
),
)
return t

@property
Expand Down
10 changes: 2 additions & 8 deletions mne/forward/forward.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,17 +225,11 @@ def __repr__(self):

@repr_html
def _repr_html_(self):
(
good_chs,
bad_chs,
_,
_,
) = self["info"]._get_chs_for_repr()
src_descr, src_ori = self._get_src_type_and_ori_for_repr()

t = _get_html_template("repr", "forward.html.jinja")
html = t.render(
good_channels=good_chs,
bad_channels=bad_chs,
info=self["info"],
source_space_descr=src_descr,
source_orientation=src_ori,
)
Expand Down
118 changes: 118 additions & 0 deletions mne/html_templates/_templates.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,118 @@
# License: BSD-3-Clause
# Copyright the MNE-Python contributors.
import datetime
import functools
import uuid
from dataclasses import dataclass
from typing import Any, Literal, Union

from .._fiff.pick import channel_type
from ..defaults import _handle_default

_COLLAPSED = False # will override in doc build


def _format_number(value: Union[int, float]) -> str:
"""Insert thousand separators."""
return f"{value:,}"


def _append_uuid(string: str, sep: str = "-") -> str:
"""Append a UUID to a string."""
return f"{string}{sep}{uuid.uuid4()}"


def _data_type(obj) -> str:
"""Return the qualified name of a class."""
return obj.__class__.__qualname__


def _dt_to_str(dt: datetime.datetime) -> str:
"""Convert a datetime object to a human-readable string representation."""
return dt.strftime("%Y-%m-%d at %H:%M:%S %Z")


def _format_baseline(inst) -> str:
"""Format the baseline time period."""
if inst.baseline is None:
baseline = "off"
else:
baseline = (
f"{round(inst.baseline[0], 3):.3f} – {round(inst.baseline[1], 3):.3f} s"
)

return baseline


def _format_time_range(inst) -> str:
"""Format evoked and epochs time range."""
tr = f"{round(inst.tmin, 3):.3f} – {round(inst.tmax, 3):.3f} s"
return tr


def _format_projs(info) -> list[str]:
"""Format projectors."""
projs = [f'{p["desc"]} ({"on" if p["active"] else "off"})' for p in info["projs"]]
return projs


@dataclass
class _Channel:
"""A channel in a recording."""

index: int
name: str
name_html: str
type: str
type_pretty: str
status: Literal["good", "bad"]


def _format_channels(info) -> dict[str, dict[Literal["good", "bad"], list[str]]]:
"""Format channel names."""
ch_types_pretty: dict[str, str] = _handle_default("titles")
channels = []

if info.ch_names:
for ch_index, ch_name in enumerate(info.ch_names):
ch_type = channel_type(info, ch_index)
ch_type_pretty = ch_types_pretty.get(ch_type, ch_type.upper())
ch_status = "bad" if ch_name in info["bads"] else "good"
channel = _Channel(
index=ch_index,
name=ch_name,
name_html=ch_name.replace(" ", "&nbsp;"),
type=ch_type,
type_pretty=ch_type_pretty,
status=ch_status,
)
channels.append(channel)

# Extract unique channel types and put them in the desired order.
ch_types = list(set([c.type_pretty for c in channels]))
ch_types = [c for c in ch_types_pretty.values() if c in ch_types]

channels_formatted = {}
for ch_type in ch_types:
goods = [c for c in channels if c.type_pretty == ch_type and c.status == "good"]
bads = [c for c in channels if c.type_pretty == ch_type and c.status == "bad"]
if ch_type not in channels_formatted:
channels_formatted[ch_type] = {"good": [], "bad": []}
channels_formatted[ch_type]["good"] = goods
channels_formatted[ch_type]["bad"] = bads

return channels_formatted


def _has_attr(obj: Any, attr: str) -> bool:
"""Check if an object has an attribute `obj.attr`.

This is needed because on dict-like objects, Jinja2's `obj.attr is defined` would
check for `obj["attr"]`, which may not be what we want.
"""
return hasattr(obj, attr)


@functools.lru_cache(maxsize=2)
def _get_html_templates_env(kind):
# For _html_repr_() and mne.Report
Expand All @@ -19,6 +127,16 @@ def _get_html_templates_env(kind):
)
if kind == "report":
templates_env.filters["zip"] = zip

templates_env.filters["format_number"] = _format_number
templates_env.filters["append_uuid"] = _append_uuid
templates_env.filters["data_type"] = _data_type
templates_env.filters["dt_to_str"] = _dt_to_str
templates_env.filters["format_baseline"] = _format_baseline
templates_env.filters["format_time_range"] = _format_time_range
templates_env.filters["format_projs"] = _format_projs
templates_env.filters["format_channels"] = _format_channels
templates_env.filters["has_attr"] = _has_attr
return templates_env


Expand Down