Skip to content

Commit

Permalink
ENH: Allow report reordering (mne-tools#12513)
Browse files Browse the repository at this point in the history
  • Loading branch information
larsoner authored Mar 26, 2024
1 parent b56420f commit eee8e6f
Show file tree
Hide file tree
Showing 3 changed files with 115 additions and 39 deletions.
2 changes: 2 additions & 0 deletions doc/changes/devel/12513.newfeature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Added the ability to reorder report contents via :meth:`mne.Report.reorder` (with
helper to get contents with :meth:`mne.Report.get_contents`), by `Eric Larson`_.
98 changes: 72 additions & 26 deletions mne/report/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
# Copyright the MNE-Python contributors.

import base64
import copy
import dataclasses
import fnmatch
import io
Expand Down Expand Up @@ -965,6 +966,64 @@ def _validate_input(self, items, captions, tag, comments=None):
)
return items, captions, comments

def copy(self):
"""Return a deepcopy of the report.
Returns
-------
report : instance of Report
The copied report.
"""
return copy.deepcopy(self)

def get_contents(self):
"""Get the content of the report.
Returns
-------
titles : list of str
The title of each content element.
tags : list of list of str
The tags for each content element, one list per element.
htmls : list of str
The HTML contents for each element.
Notes
-----
.. versionadded:: 1.7
"""
htmls, _, titles, tags = self._content_as_html()
return titles, tags, htmls

def reorder(self, order):
"""Reorder the report content.
Parameters
----------
order : array-like of int
The indices of the new order (as if you were reordering an array).
For example if there are 4 elements in the report,
``order=[3, 0, 1, 2]`` would take the last element and move it to
the front. In other words, ``elements = [elements[ii] for ii in order]]``.
Notes
-----
.. versionadded:: 1.7
"""
_validate_type(order, "array-like", "order")
order = np.array(order)
if order.dtype.kind != "i" or order.ndim != 1:
raise ValueError(
"order must be an array of integers, got "
f"{order.ndim}D array of dtype {order.dtype}"
)
n_elements = len(self._content)
if not np.array_equal(np.sort(order), np.arange(n_elements)):
raise ValueError(
f"order must be a permutation of range({n_elements}), got:\n{order}"
)
self._content = [self._content[ii] for ii in order]

def _content_as_html(self):
"""Generate HTML representations based on the added content & sections.
Expand Down Expand Up @@ -1039,18 +1098,12 @@ def _content_as_html(self):
@property
def html(self):
"""A list of HTML representations for all content elements."""
htmls, _, _, _ = self._content_as_html()
return htmls
return self._content_as_html()[0]

@property
def tags(self):
"""All tags currently used in the report."""
tags = []
for c in self._content:
tags.extend(c.tags)

tags = tuple(sorted(set(tags)))
return tags
"""A sorted tuple of all tags currently used in the report."""
return tuple(sorted(set(sum(self._content_as_html()[3], ()))))

def add_custom_css(self, css):
"""Add custom CSS to the report.
Expand Down Expand Up @@ -2875,7 +2928,7 @@ def parse_folder(
)

if sort_content:
self._content = self._sort(content=self._content, order=CONTENT_ORDER)
self._sort(order=CONTENT_ORDER)

def __getstate__(self):
"""Get the state of the report as a dictionary."""
Expand Down Expand Up @@ -2954,7 +3007,7 @@ def save(
fname = op.realpath(fname) # resolve symlinks

if sort_content:
self._content = self._sort(content=self._content, order=CONTENT_ORDER)
self._sort(order=CONTENT_ORDER)

if not overwrite and op.isfile(fname):
msg = (
Expand Down Expand Up @@ -3017,30 +3070,23 @@ def __exit__(self, exception_type, value, traceback):
if self.fname is not None:
self.save(self.fname, open_browser=False, overwrite=True)

@staticmethod
def _sort(content, order):
def _sort(self, *, order):
"""Reorder content to reflect "natural" ordering."""
content_unsorted = content.copy()
content_sorted = []
content_sorted_idx = []
del content

# First arrange content with known tags in the predefined order
for tag in order:
for idx, content in enumerate(content_unsorted):
for idx, content in enumerate(self._content):
if tag in content.tags:
content_sorted_idx.append(idx)
content_sorted.append(content)

# Now simply append the rest (custom tags)
content_remaining = [
content
for idx, content in enumerate(content_unsorted)
if idx not in content_sorted_idx
]

content_sorted = [*content_sorted, *content_remaining]
return content_sorted
self.reorder(
np.r_[
content_sorted_idx,
np.setdiff1d(np.arange(len(self._content)), content_sorted_idx),
]
)

def _render_one_bem_axis(
self,
Expand Down
54 changes: 41 additions & 13 deletions mne/report/tests/test_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
# Copyright the MNE-Python contributors.

import base64
import copy
import glob
import os
import pickle
Expand Down Expand Up @@ -638,7 +637,7 @@ def test_remove():
r.add_figure(fig=fig2, title="figure2", tags=("slider",))

# Test removal by title
r2 = copy.deepcopy(r)
r2 = r.copy()
removed_index = r2.remove(title="figure1")
assert removed_index == 2
assert len(r2.html) == 3
Expand All @@ -647,7 +646,7 @@ def test_remove():
assert r2.html[2] == r.html[3]

# Test restricting to section
r2 = copy.deepcopy(r)
r2 = r.copy()
removed_index = r2.remove(title="figure1", tags=("othertag",))
assert removed_index == 1
assert len(r2.html) == 3
Expand Down Expand Up @@ -692,7 +691,7 @@ def test_add_or_replace(tags):
assert len(r.html) == 4
assert len(r._content) == 4

old_r = copy.deepcopy(r)
old_r = r.copy()

# Replace our last occurrence of title='duplicate'
r.add_figure(
Expand Down Expand Up @@ -765,7 +764,7 @@ def test_add_or_replace_section():
assert len(r.html) == 3
assert len(r._content) == 3

old_r = copy.deepcopy(r)
old_r = r.copy()
assert r.html[0] == old_r.html[0]
assert r.html[1] == old_r.html[1]
assert r.html[2] == old_r.html[2]
Expand Down Expand Up @@ -1108,24 +1107,53 @@ def test_sorting(tmp_path):
"""Test that automated ordering based on tags works."""
r = Report()

r.add_code(code="E = m * c**2", title="intelligence >9000", tags=("bem",))
r.add_code(code="a**2 + b**2 = c**2", title="Pythagoras", tags=("evoked",))
r.add_code(code="🧠", title="source of truth", tags=("source-estimate",))
r.add_code(code="🥦", title="veggies", tags=("raw",))
titles = ["intelligence >9000", "Pythagoras", "source of truth", "veggies"]
r.add_code(code="E = m * c**2", title=titles[0], tags=("bem",))
r.add_code(code="a**2 + b**2 = c**2", title=titles[1], tags=("evoked",))
r.add_code(code="🧠", title=titles[2], tags=("source-estimate",))
r.add_code(code="🥦", title=titles[3], tags=("raw",))

# Check that repeated calls of add_* actually continuously appended to
# the report
orig_order = ["bem", "evoked", "source-estimate", "raw"]
assert [c.tags[0] for c in r._content] == orig_order

# tags property behavior and get_contents
assert list(r.tags) == sorted(orig_order)
titles, tags, htmls = r.get_contents()
assert set(sum(tags, ())) == set(r.tags)
assert len(titles) == len(tags) == len(htmls) == len(r._content)
for title, tag, html in zip(titles, tags, htmls):
title = title.replace(">", ">")
assert title in html
for t in tag:
assert t in html

# Now check the actual sorting
content_sorted = r._sort(content=r._content, order=CONTENT_ORDER)
r_sorted = r.copy()
r_sorted._sort(order=CONTENT_ORDER)
expected_order = ["raw", "evoked", "bem", "source-estimate"]

assert content_sorted != r._content
assert [c.tags[0] for c in content_sorted] == expected_order
assert r_sorted._content != r._content
assert [c.tags[0] for c in r_sorted._content] == expected_order
assert [c.tags[0] for c in r._content] == orig_order

r.copy().save(fname=tmp_path / "report.html", sort_content=True, open_browser=False)

# Manual sorting should be the same
r_sorted = r.copy()
order = np.argsort([CONTENT_ORDER.index(t) for t in orig_order])
r_sorted.reorder(order)

assert r_sorted._content != r._content
got_order = [c.tags[0] for c in r_sorted._content]
assert [c.tags[0] for c in r._content] == orig_order # original unmodified
assert got_order == expected_order

r.save(fname=tmp_path / "report.html", sort_content=True, open_browser=False)
with pytest.raises(ValueError, match="order must be a permutation"):
r.reorder(np.arange(len(r._content) + 1))
with pytest.raises(ValueError, match="array of integers"):
r.reorder([1.0])


@pytest.mark.parametrize(
Expand Down

0 comments on commit eee8e6f

Please sign in to comment.