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

1"""Generate HTML reports.""" 

2 

3import uuid 

4import warnings 

5from string import Template 

6 

7import pandas as pd 

8 

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) 

26 

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} 

38 

39 

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. 

44 

45 Parameters 

46 ---------- 

47 estimator : object instance of BaseEstimator 

48 The object we wish to retrieve template of. 

49 

50 Returns 

51 ------- 

52 template : str 

53 Name of the template file to use. 

54 

55 """ 

56 if estimator.__class__.__name__ in ESTIMATOR_TEMPLATES: 

57 return ESTIMATOR_TEMPLATES[estimator.__class__.__name__] 

58 else: 

59 return ESTIMATOR_TEMPLATES["default"] 

60 

61 

62def embed_img(display): 

63 """Embed an image or just return its instance if already embedded. 

64 

65 Parameters 

66 ---------- 

67 display : obj 

68 A Nilearn plotting object to display. 

69 

70 Returns 

71 ------- 

72 embed : str 

73 Binary image string. 

74 

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) 

82 

83 

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. 

96 

97 Parameters 

98 ---------- 

99 title : str 

100 The title for the report. 

101 

102 docstring : str 

103 The introductory docstring for the reported object. 

104 

105 content : img 

106 The content to display. 

107 

108 overlay : img 

109 Overlaid content, to appear on hover. 

110 

111 parameters : dict 

112 A dictionary of object parameters and their values. 

113 

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. 

128 

129 summary_html : dict if estimator is Surface masker str otherwise, optional 

130 Summary of the region labels and sizes converted to html table. 

131 

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. 

136 

137 Returns 

138 ------- 

139 report : HTMLReport 

140 An instance of a populated HTML report. 

141 

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 ) 

153 

154 with (JS_PATH / "carousel.js").open(encoding="utf-8") as js_file: 

155 js_carousel = js_file.read() 

156 

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() 

160 

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"] = "" 

167 

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 ) 

190 

191 # revert HTML safe substitutions in CSS sections 

192 body = body.replace(".pure-g > div", ".pure-g > div") 

193 

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()) 

198 

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() 

202 

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 ) 

213 

214 

215def _define_overlay(estimator): 

216 """Determine whether an overlay was provided and \ 

217 update the report text as appropriate. 

218 """ 

219 displays = estimator._reporting() 

220 

221 if len(displays) == 1: # set overlay to None 

222 return None, displays[0] 

223 

224 elif isinstance(estimator, NiftiSpheresMasker): 

225 return None, displays 

226 

227 elif len(displays) == 2: 

228 return displays[0], displays[1] 

229 

230 return None, displays 

231 

232 

233def generate_report(estimator): 

234 """Generate a report for Nilearn objects. 

235 

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. 

239 

240 Parameters 

241 ---------- 

242 estimator : Object instance of BaseEstimator. 

243 Object for which the report should be generated. 

244 

245 Returns 

246 ------- 

247 report : HTMLReport 

248 

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] 

262 

263 if hasattr(estimator, "_report_content"): 

264 data = estimator._report_content 

265 else: 

266 data = {} 

267 

268 warning_messages = [] 

269 

270 if estimator.reports is False: 

271 warning_messages.append( 

272 "\nReport generation not enabled!\nNo visual outputs created." 

273 ) 

274 

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 ) 

283 

284 if warning_messages: 

285 for msg in warning_messages: 

286 warnings.warn( 

287 msg, 

288 stacklevel=find_stack_level(), 

289 ) 

290 

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 ) 

300 

301 return _create_report(estimator, data) 

302 

303 

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 ) 

316 

317 

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) 

325 

326 

327def _create_report(estimator, data): 

328 html_template = _get_estimator_template(estimator) 

329 

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 ) 

338 

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] 

377 

378 # Generate a unique ID for this report 

379 unique_id = str(uuid.uuid4()).replace("-", "") 

380 

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 ) 

391 

392 

393def is_notebook() -> bool: 

394 """Detect if we are running in a notebook. 

395 

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 

403 

404 

405class HTMLReport(HTMLDocument): 

406 """A report written as HTML. 

407 

408 Methods such as ``save_as_html``, or ``open_in_browser`` 

409 are inherited from class ``nilearn.plotting.html_document.HTMLDocument``. 

410 

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: 

417 

418 - title: The title of the HTML page. 

419 - body: The full body of the HTML page. Provided through 

420 the ``body`` input. 

421 

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. 

426 

427 head_values : :obj:`dict`, default=None 

428 Additional substitutions in ``head_tpl``. 

429 if ``None`` is passed, defaults to ``{}`` 

430 

431 .. note:: 

432 This can be used to provide additional values 

433 with custom templates. 

434 

435 """ 

436 

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 

445 

446 def _repr_html_(self): 

447 """Return body of the report. 

448 

449 Method used by the Jupyter notebook. 

450 Users normally won't call this method explicitly. 

451 """ 

452 return self.body 

453 

454 def __str__(self): 

455 return self.body