Coverage for nilearn/reporting/html_report.py: 15%
129 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-20 10:58 +0200
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-20 10:58 +0200
1"""Generate HTML reports."""
3import uuid
4import warnings
5from string import Template
7import pandas as pd
9from nilearn._utils.helpers import is_matplotlib_installed
10from nilearn._utils.html_document import HTMLDocument
11from nilearn._utils.logger import find_stack_level
12from nilearn._version import __version__
13from nilearn.externals import tempita
14from nilearn.maskers import NiftiSpheresMasker
15from nilearn.reporting._utils import (
16 dataframe_to_html,
17 model_attributes_to_dataframe,
18)
19from nilearn.reporting.utils import (
20 CSS_PATH,
21 HTML_PARTIALS_PATH,
22 HTML_TEMPLATE_PATH,
23 JS_PATH,
24 figure_to_svg_base64,
25)
27ESTIMATOR_TEMPLATES = {
28 "NiftiLabelsMasker": "report_body_template_niftilabelsmasker.html",
29 "MultiNiftiLabelsMasker": "report_body_template_niftilabelsmasker.html",
30 "NiftiMapsMasker": "report_body_template_niftimapsmasker.html",
31 "MultiNiftiMapsMasker": "report_body_template_niftimapsmasker.html",
32 "NiftiSpheresMasker": "report_body_template_niftispheresmasker.html",
33 "SurfaceMasker": "report_body_template_surfacemasker.html",
34 "SurfaceLabelsMasker": "report_body_template_surfacemasker.html",
35 "SurfaceMapsMasker": "report_body_template_surfacemapsmasker.html",
36 "default": "report_body_template.html",
37}
40def _get_estimator_template(estimator):
41 """Return the HTML template to use for a given estimator \
42 if a specific template was defined in ESTIMATOR_TEMPLATES, \
43 otherwise return the default template.
45 Parameters
46 ----------
47 estimator : object instance of BaseEstimator
48 The object we wish to retrieve template of.
50 Returns
51 -------
52 template : str
53 Name of the template file to use.
55 """
56 if estimator.__class__.__name__ in ESTIMATOR_TEMPLATES:
57 return ESTIMATOR_TEMPLATES[estimator.__class__.__name__]
58 else:
59 return ESTIMATOR_TEMPLATES["default"]
62def embed_img(display):
63 """Embed an image or just return its instance if already embedded.
65 Parameters
66 ----------
67 display : obj
68 A Nilearn plotting object to display.
70 Returns
71 -------
72 embed : str
73 Binary image string.
75 """
76 if display is None: # no image to display
77 return None
78 # If already embedded, simply return as is
79 if isinstance(display, str):
80 return display
81 return figure_to_svg_base64(display.frame_axes.figure)
84def _update_template(
85 title,
86 docstring,
87 content,
88 overlay,
89 parameters,
90 data,
91 summary_html=None,
92 template_name=None,
93 warning_messages=None,
94):
95 """Populate a report with content.
97 Parameters
98 ----------
99 title : str
100 The title for the report.
102 docstring : str
103 The introductory docstring for the reported object.
105 content : img
106 The content to display.
108 overlay : img
109 Overlaid content, to appear on hover.
111 parameters : dict
112 A dictionary of object parameters and their values.
114 data : dict
115 A dictionary holding the data to be added to the report.
116 The keys must match exactly the ones used in the template.
117 The default template accepts the following:
118 - description (str) : Description of the content.
119 - warning_message (str) : An optional warning
120 message to be displayed in red. This is used
121 for example when no image was provided to the
122 estimator when fitting.
123 The NiftiLabelsMasker template accepts the additional
124 fields:
125 - summary (dict) : A summary description of the
126 region labels and sizes. This will be displayed
127 as an expandable table in the report.
129 summary_html : dict if estimator is Surface masker str otherwise, optional
130 Summary of the region labels and sizes converted to html table.
132 template_name : str, optional
133 The name of the template to use. If not provided, the
134 default template `report_body_template.html` will be
135 used.
137 Returns
138 -------
139 report : HTMLReport
140 An instance of a populated HTML report.
142 """
143 if template_name is None:
144 body_template_name = "report_body_template.html"
145 else:
146 body_template_name = template_name
147 body_template_path = HTML_TEMPLATE_PATH / body_template_name
148 if not body_template_path.exists():
149 raise FileNotFoundError(f"No template {body_template_path}")
150 tpl = tempita.HTMLTemplate.from_filename(
151 str(body_template_path), encoding="utf-8"
152 )
154 with (JS_PATH / "carousel.js").open(encoding="utf-8") as js_file:
155 js_carousel = js_file.read()
157 css_file_path = CSS_PATH / "masker_report.css"
158 with css_file_path.open(encoding="utf-8") as css_file:
159 css = css_file.read()
161 if "n_elements" not in data:
162 data["n_elements"] = 0
163 if "coverage" in data:
164 data["coverage"] = f"{data['coverage']:0.1f}"
165 else:
166 data["coverage"] = ""
168 body = tpl.substitute(
169 title=title,
170 content=content,
171 overlay=overlay,
172 docstring=docstring,
173 parameters=parameters,
174 figure=(
175 _insert_figure_partial(
176 data["engine"],
177 content,
178 data["displayed_maps"],
179 data["unique_id"],
180 )
181 if "engine" in data
182 else None
183 ),
184 **data,
185 css=css,
186 js_carousel=js_carousel,
187 warning_messages=_render_warnings_partial(warning_messages),
188 summary_html=summary_html,
189 )
191 # revert HTML safe substitutions in CSS sections
192 body = body.replace(".pure-g > div", ".pure-g > div")
194 head_template_name = "report_head_template.html"
195 head_template_path = HTML_TEMPLATE_PATH / head_template_name
196 with head_template_path.open() as head_file:
197 head_tpl = Template(head_file.read())
199 head_css_file_path = CSS_PATH / "head.css"
200 with head_css_file_path.open(encoding="utf-8") as head_css_file:
201 head_css = head_css_file.read()
203 return HTMLReport(
204 body=body,
205 head_tpl=head_tpl,
206 head_values={
207 "head_css": head_css,
208 "version": __version__,
209 "page_title": f"{title} report",
210 "display_footer": "style='display: none'" if is_notebook() else "",
211 },
212 )
215def _define_overlay(estimator):
216 """Determine whether an overlay was provided and \
217 update the report text as appropriate.
218 """
219 displays = estimator._reporting()
221 if len(displays) == 1: # set overlay to None
222 return None, displays[0]
224 elif isinstance(estimator, NiftiSpheresMasker):
225 return None, displays
227 elif len(displays) == 2:
228 return displays[0], displays[1]
230 return None, displays
233def generate_report(estimator):
234 """Generate a report for Nilearn objects.
236 Reports are useful to visualize steps in a processing pipeline.
237 Example use case: visualize the overlap of a mask and reference image
238 in NiftiMasker.
240 Parameters
241 ----------
242 estimator : Object instance of BaseEstimator.
243 Object for which the report should be generated.
245 Returns
246 -------
247 report : HTMLReport
249 """
250 if not is_matplotlib_installed():
251 with warnings.catch_warnings():
252 mpl_unavail_msg = (
253 "Matplotlib is not imported! No reports will be generated."
254 )
255 warnings.filterwarnings("always", message=mpl_unavail_msg)
256 warnings.warn(
257 category=ImportWarning,
258 message=mpl_unavail_msg,
259 stacklevel=find_stack_level(),
260 )
261 return [None]
263 if hasattr(estimator, "_report_content"):
264 data = estimator._report_content
265 else:
266 data = {}
268 warning_messages = []
270 if estimator.reports is False:
271 warning_messages.append(
272 "\nReport generation not enabled!\nNo visual outputs created."
273 )
275 if (
276 not hasattr(estimator, "_reporting_data")
277 or not estimator._reporting_data
278 ):
279 warning_messages.append(
280 "\nThis report was not generated.\n"
281 "Make sure to run `fit` before inspecting reports."
282 )
284 if warning_messages:
285 for msg in warning_messages:
286 warnings.warn(
287 msg,
288 stacklevel=find_stack_level(),
289 )
291 return _update_template(
292 title="Empty Report",
293 docstring="Empty Report",
294 content=embed_img(None),
295 overlay=None,
296 parameters={},
297 data=data,
298 warning_messages=warning_messages,
299 )
301 return _create_report(estimator, data)
304def _insert_figure_partial(engine, content, displayed_maps, unique_id=None):
305 tpl = tempita.HTMLTemplate.from_filename(
306 str(HTML_PARTIALS_PATH / "figure.html"), encoding="utf-8"
307 )
308 if not isinstance(content, list):
309 content = [content]
310 return tpl.substitute(
311 engine=engine,
312 content=content,
313 displayed_maps=displayed_maps,
314 unique_id=unique_id,
315 )
318def _render_warnings_partial(warning_messages):
319 if not warning_messages:
320 return ""
321 tpl = tempita.HTMLTemplate.from_filename(
322 str(HTML_PARTIALS_PATH / "warnings.html"), encoding="utf-8"
323 )
324 return tpl.substitute(warning_messages=warning_messages)
327def _create_report(estimator, data):
328 html_template = _get_estimator_template(estimator)
330 # note that some surface images are passed via data
331 # for surface maps masker
332 overlay, image = _define_overlay(estimator)
333 embeded_images = (
334 [embed_img(i) for i in image]
335 if isinstance(image, list)
336 else embed_img(image)
337 )
339 summary_html = None
340 # only convert summary to html table if summary exists
341 if "summary" in data and data["summary"] is not None:
342 # convert region summary to html table
343 # for Surface maskers create a table for each part
344 if "Surface" in estimator.__class__.__name__:
345 summary_html = {}
346 for part in data["summary"]:
347 summary_html[part] = pd.DataFrame.from_dict(
348 data["summary"][part]
349 )
350 summary_html[part] = dataframe_to_html(
351 summary_html[part],
352 precision=2,
353 header=True,
354 index=False,
355 sparsify=False,
356 )
357 # otherwise we just have one table
358 elif "Nifti" in estimator.__class__.__name__:
359 summary_html = pd.DataFrame.from_dict(data["summary"])
360 summary_html = dataframe_to_html(
361 summary_html,
362 precision=2,
363 header=True,
364 index=False,
365 sparsify=False,
366 )
367 parameters = model_attributes_to_dataframe(estimator)
368 with pd.option_context("display.max_colwidth", 100):
369 parameters = dataframe_to_html(
370 parameters,
371 precision=2,
372 header=True,
373 sparsify=False,
374 )
375 docstring = estimator.__doc__
376 snippet = docstring.partition("Parameters\n ----------\n")[0]
378 # Generate a unique ID for this report
379 unique_id = str(uuid.uuid4()).replace("-", "")
381 return _update_template(
382 title=estimator.__class__.__name__,
383 docstring=snippet,
384 content=embeded_images,
385 overlay=embed_img(overlay),
386 parameters=parameters,
387 data={**data, "unique_id": unique_id},
388 template_name=html_template,
389 summary_html=summary_html,
390 )
393def is_notebook() -> bool:
394 """Detect if we are running in a notebook.
396 From https://stackoverflow.com/questions/15411967/how-can-i-check-if-code-is-executed-in-the-ipython-notebook
397 """
398 try:
399 shell = get_ipython().__class__.__name__ # type: ignore[name-defined]
400 return shell == "ZMQInteractiveShell"
401 except NameError:
402 return False # Probably standard Python interpreter
405class HTMLReport(HTMLDocument):
406 """A report written as HTML.
408 Methods such as ``save_as_html``, or ``open_in_browser``
409 are inherited from class ``nilearn.plotting.html_document.HTMLDocument``.
411 Parameters
412 ----------
413 head_tpl : Template
414 This is meant for display as a full page, eg writing on disk.
415 This is the Template object used to generate the HTML head
416 section of the report. The template should be filled with:
418 - title: The title of the HTML page.
419 - body: The full body of the HTML page. Provided through
420 the ``body`` input.
422 body : :obj:`str`
423 This parameter is used for embedding in the provided
424 ``head_tpl`` template. It contains the full body of the
425 HTML page.
427 head_values : :obj:`dict`, default=None
428 Additional substitutions in ``head_tpl``.
429 if ``None`` is passed, defaults to ``{}``
431 .. note::
432 This can be used to provide additional values
433 with custom templates.
435 """
437 def __init__(self, head_tpl, body, head_values=None):
438 """Construct the ``HTMLReport`` class."""
439 if head_values is None:
440 head_values = {}
441 html = head_tpl.safe_substitute(body=body, **head_values)
442 super().__init__(html)
443 self.head_tpl = head_tpl
444 self.body = body
446 def _repr_html_(self):
447 """Return body of the report.
449 Method used by the Jupyter notebook.
450 Users normally won't call this method explicitly.
451 """
452 return self.body
454 def __str__(self):
455 return self.body