Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/changes/devel.rst
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ Bugs
- Fix bug with :meth:`~mne.viz.Brain.add_annotation` when reading an annotation from a file with both hemispheres shown (:gh:`11946` by `Marijn van Vliet`_)
- Fix bug with axis clip box boundaries in :func:`mne.viz.plot_evoked_topo` and related functions (:gh:`11999` by `Eric Larson`_)
- Fix bug with ``subject_info`` when loading data from and exporting to EDF file (:gh:`11952` by `Paul Roujansky`_)
- Fix bug where :class:`mne.Info` HTML representations listed all channel counts instead of good channel counts under the heading "Good channels" (:gh:`12145` by `Eric Larson`_)
- Fix rendering glitches when plotting Neuromag/TRIUX sensors in :func:`mne.viz.plot_alignment` and related functions (:gh:`12098` by `Eric Larson`_)
- Fix bug with delayed checking of :class:`info["bads"] <mne.Info>` (:gh:`12038` by `Eric Larson`_)
- Fix bug with :func:`mne.viz.plot_alignment` where ``sensor_colors`` were not handled properly on a per-channel-type basis (:gh:`12067` by `Eric Larson`_)
Expand Down
2 changes: 2 additions & 0 deletions doc/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from numpydoc import docscrape

import mne
import mne.html_templates._templates
from mne.tests.test_docstring_parameters import error_ignores
from mne.utils import (
linkcode_resolve, # noqa, analysis:ignore
Expand All @@ -41,6 +42,7 @@
# https://numba.readthedocs.io/en/latest/reference/deprecation.html#deprecation-of-old-style-numba-captured-errors # noqa: E501
os.environ["NUMBA_CAPTURED_ERRORS"] = "new_style"
sphinx_logger = sphinx.util.logging.getLogger("mne")
mne.html_templates._templates._COLLAPSED = True # collapse info _repr_html_

# -- Path setup --------------------------------------------------------------

Expand Down
45 changes: 15 additions & 30 deletions mne/_fiff/meas_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@
import datetime
import operator
import string
import uuid
from collections import Counter, OrderedDict
from collections import Counter, OrderedDict, defaultdict
from collections.abc import Mapping
from copy import deepcopy
from io import BytesIO
Expand Down Expand Up @@ -1852,37 +1851,25 @@ def _get_chs_for_repr(self):
titles = _handle_default("titles")

# good channels
channels = {}
ch_types = [channel_type(self, idx) for idx in range(len(self["chs"]))]
ch_counts = Counter(ch_types)
for ch_type, count in ch_counts.items():
if ch_type == "meg":
channels["mag"] = len(pick_types(self, meg="mag"))
channels["grad"] = len(pick_types(self, meg="grad"))
elif ch_type == "eog":
pick_eog = pick_types(self, eog=True)
eog = ", ".join(np.array(self["ch_names"])[pick_eog])
elif ch_type == "ecg":
pick_ecg = pick_types(self, ecg=True)
ecg = ", ".join(np.array(self["ch_names"])[pick_ecg])
channels[ch_type] = count

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"{v} {titles.get(k, k.upper())}" for k, v in channels.items()]
[f"{len(v)} {titles.get(k, k.upper())}" for k, v in good_names.items()]
)

if "ecg" not in channels.keys():
ecg = "Not available"
if "eog" not in channels.keys():
eog = "Not available"
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
if len(self["bads"]) > 0:
bad_channels = ", ".join(self["bads"])
else:
bad_channels = "None"
bad_channels = ", ".join(self["bads"]) or "None"

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

@repr_html
def _repr_html_(self, caption=None, duration=None, filenames=None):
Expand Down Expand Up @@ -1918,10 +1905,8 @@ def _repr_html_(self, caption=None, duration=None, filenames=None):

info_template = _get_html_template("repr", "info.html.jinja")
sections = ("General", "Channels", "Data")
section_ids = [f"section_{str(uuid.uuid4())}" for _ in sections]
return html + info_template.render(
sections=sections,
section_ids=section_ids,
caption=caption,
meas_date=meas_date,
projs=projs,
Expand Down
6 changes: 3 additions & 3 deletions mne/_fiff/tests/test_meas_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -899,11 +899,11 @@ def test_repr_html():
assert "EEG 053" in info._repr_html_()

html = info._repr_html_()
for ch in [
"204 Gradiometers",
for ch in [ # good channel counts
"203 Gradiometers",
"102 Magnetometers",
"9 Stimulus",
"60 EEG",
"59 EEG",
"1 EOG",
]:
assert ch in html
Expand Down
18 changes: 17 additions & 1 deletion mne/html_templates/_templates.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import functools

_COLLAPSED = False # will override in doc build


@functools.lru_cache(maxsize=2)
def _get_html_templates_env(kind):
Expand All @@ -19,4 +21,18 @@ def _get_html_templates_env(kind):


def _get_html_template(kind, name):
return _get_html_templates_env(kind).get_template(name)
return _RenderWrap(
_get_html_templates_env(kind).get_template(name),
collapsed=_COLLAPSED,
)


class _RenderWrap:
"""Class that allows functools.partial-like wrapping of jinja2 Template.render()."""

def __init__(self, template, **kwargs):
self._template = template
self._kwargs = kwargs

def render(self, *args, **kwargs):
return self._template.render(*args, **kwargs, **self._kwargs)
1 change: 1 addition & 0 deletions mne/html_templates/repr/evoked.html.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<th>Timepoints</th>
<td>{{ evoked.data.shape[1] }} samples</td>
</tr>
<tr>
<th>Channels</th>
<td>{{ evoked.data.shape[0] }} channels</td>
</tr>
Expand Down
239 changes: 98 additions & 141 deletions mne/html_templates/repr/info.html.jinja
Original file line number Diff line number Diff line change
@@ -1,144 +1,101 @@
<style>
label { cursor: pointer; }
#{{ section_ids[0] }} ~ table [for="{{ section_ids[0] }}"]::before,
#{{ section_ids[1] }} ~ table [for="{{ section_ids[1] }}"]::before,
#{{ section_ids[2] }} ~ table [for="{{ section_ids[2] }}"]::before {
display: inline-block;
content: "►";
font-size: 11px;
width: 15px;
text-align: left;
}

#{{ section_ids[0] }}:checked ~ table [for="{{ section_ids[0] }}"]::before,
#{{ section_ids[1] }}:checked ~ table [for="{{ section_ids[1] }}"]::before,
#{{ section_ids[2] }}:checked ~ table [for="{{ section_ids[2] }}"]::before {
content: "▼";
}

#{{ section_ids[0] }} ~ table tr.{{ section_ids[0] }},
#{{ section_ids[1] }} ~ table tr.{{ section_ids[1] }},
#{{ section_ids[2] }} ~ table tr.{{ section_ids[2] }} {
visibility: collapse;
}

#{{ section_ids[0] }}:checked ~ table tr.{{ section_ids[0] }},
#{{ section_ids[1] }}:checked ~ table tr.{{ section_ids[1] }},
#{{ section_ids[2] }}:checked ~ table tr.{{ section_ids[2] }} {
visibility: visible;
}
</style>

<input type="checkbox" id="{{ section_ids[0] }}" hidden aria-hidden="true"/>
<input type="checkbox" id="{{ section_ids[1] }}" hidden aria-hidden="true"/>
<input type="checkbox" id="{{ section_ids[2] }}" hidden aria-hidden="true"/>

<table class="table table-hover table-striped table-sm table-responsive small">
<tr>
<th class="collapsible_header" colspan="2" style="font-weight: bold; text-align: left;">
<label for={{ section_ids[0] }}>
{{sections[0]}}
</label>
</th>
</tr>
<tr class="{{ section_ids[0] }}">
<th>Measurement date</th>
{% if meas_date is not none %}
<td>{{ meas_date }}</td>
{% else %}
<td>Unknown</td>
{% endif %}
</tr>
<tr class="{{ section_ids[0] }}">
<th>Experimenter</th>
{% if experimenter is not none %}
<td>{{ experimenter }}</td>
{% else %}
<td>Unknown</td>
{% endif %}
</tr>
<tr class="{{ section_ids[0] }}">
<th>Participant</th>
{% if subject_info is defined and subject_info is not none %}
<details{{ "" if collapsed else " open" }}>
<summary><strong>{{sections[0]}}</strong></summary>
<table class="table table-hover table-striped table-sm table-responsive small">
<tr>
<th>Measurement date</th>
{% if meas_date is not none %}
<td>{{ meas_date }}</td>
{% else %}
<td>Unknown</td>
{% endif %}
</tr>
<tr>
<th>Experimenter</th>
{% if experimenter is not none %}
<td>{{ experimenter }}</td>
{% else %}
<td>Unknown</td>
{% endif %}
</tr>
<tr>
<th>Participant</th>
{% if subject_info is defined and subject_info is not none %}
{% if 'his_id' in subject_info.keys() %}
<td>{{ subject_info['his_id'] }}</td>
{% endif %}
{% else %}
<td>Unknown</td>
{% endif %}
</tr>
<tr>
<th class="collapsible_header" colspan="2" style="font-weight: bold; text-align: left;">
<label for={{ section_ids[1] }}>
{{sections[1]}}
</label>
</th>
</tr>
<tr class="{{ section_ids[1] }}">
<th>Digitized points</th>
{% if dig is not none %}
<td>{{ dig|length }} points</td>
{% else %}
<td>Not available</td>
{% endif %}
</tr>
<tr class="{{ section_ids[1] }}">
<th>Good channels</th>
<td>{{ good_channels }}</td>
</tr>
<tr class="{{ section_ids[1] }}">
<th>Bad channels</th>
<td>{{ bad_channels }}</td>
</tr>
<tr class="{{ section_ids[1] }}">
<th>EOG channels</th>
<td>{{ eog }}</td>
</tr>
<tr class="{{ section_ids[1] }}">
<th>ECG channels</th>
<td>{{ ecg }}</td>
</tr>
<tr>
<th class="collapsible_header" colspan="2" style="font-weight: bold; text-align: left;">
<label for={{ section_ids[2] }}>
{{sections[2]}}
</label>
</th>
</tr>
{% if sfreq is not none %}
<tr class="{{ section_ids[2] }}">
<th>Sampling frequency</th>
<td>{{ '%0.2f'|format(sfreq) }} Hz</td>
</tr>
{% endif %}
{% if highpass is not none %}
<tr class="{{ section_ids[2] }}">
<th>Highpass</th>
<td>{{ '%0.2f'|format(highpass) }} Hz</td>
</tr>
{% endif %}
{% if lowpass is not none %}
<tr class="{{ section_ids[2] }}">
<th>Lowpass</th>
<td>{{ '%0.2f'|format(lowpass) }} Hz</td>
</tr>
{% endif %}
{% if projs is not none %}
<tr class="{{ section_ids[2] }}">
<th>Projections</th>
<td>{{ projs|join('<br/>') | safe }}</td>
</tr>
{% endif %}
{% if filenames %}
<tr class="{{section_ids[2]}}">
<th>Filenames</th>
<td>{{ filenames|join('<br>') }}</td>
</tr>
{% endif %}
{% if duration %}
<tr class="{{section_ids[2]}}">
<th>Duration</th>
<td>{{ duration }} (HH:MM:SS)</td>
</tr>
{% endif %}
</table>
{% else %}
<td>Unknown</td>
{% endif %}
</tr>
</table>
</details>
<details{{ "" if collapsed else " open" }}>
<summary><strong>{{sections[1]}}</strong></summary>
<table class="table table-hover table-striped table-sm table-responsive small">
<tr>
<th>Digitized points</th>
{% if dig is not none %}
<td>{{ dig|length }} points</td>
{% else %}
<td>Not available</td>
{% endif %}
</tr>
<tr>
<th>Good channels</th>
<td>{{ good_channels }}</td>
</tr>
<tr>
<th>Bad channels</th>
<td>{{ bad_channels }}</td>
</tr>
<tr>
<th>EOG channels</th>
<td>{{ eog }}</td>
</tr>
<tr>
<th>ECG channels</th>
<td>{{ ecg }}</td>
</tr>
</table>
</details>
<details{{ "" if collapsed else " open" }}>
<summary><strong>{{sections[2]}}</strong></summary>
<table class="table table-hover table-striped table-sm table-responsive small">
{% if sfreq is not none %}
<tr>
<th>Sampling frequency</th>
<td>{{ '%0.2f'|format(sfreq) }} Hz</td>
</tr>
{% endif %}
{% if highpass is not none %}
<tr>
<th>Highpass</th>
<td>{{ '%0.2f'|format(highpass) }} Hz</td>
</tr>
{% endif %}
{% if lowpass is not none %}
<tr>
<th>Lowpass</th>
<td>{{ '%0.2f'|format(lowpass) }} Hz</td>
</tr>
{% endif %}
{% if projs is not none %}
<tr>
<th>Projections</th>
<td>{{ projs|join('<br />') | safe }}</td>
</tr>
{% endif %}
{% if filenames %}
<tr>
<th>Filenames</th>
<td>{{ filenames|join('<br>') }}</td>
</tr>
{% endif %}
{% if duration %}
<tr>
<th>Duration</th>
<td>{{ duration }} (HH:MM:SS)</td>
</tr>
{% endif %}
</table>
</details>
2 changes: 1 addition & 1 deletion mne/io/tests/test_raw.py
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,7 @@ def _test_raw_reader(
assert meas_date is None or meas_date >= _stamp_to_dt((0, 0))

# test repr_html
assert "Good channels" in raw.info._repr_html_()
assert "Good channels" in raw._repr_html_()

# test resetting raw
if test_kwargs:
Expand Down
Loading