Coverage for nilearn/_utils/html_document.py: 30%
114 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"""Handle HTML plotting."""
3import warnings
4import weakref
5import webbrowser
6from html import escape
7from http import HTTPStatus
8from http.server import BaseHTTPRequestHandler
9from pathlib import Path
10from queue import Empty, Queue
11from socketserver import TCPServer
12from threading import Thread
14from nilearn._utils import remove_parameters
15from nilearn._utils.logger import find_stack_level
17MAX_IMG_VIEWS_BEFORE_WARNING = 10
18BROWSER_TIMEOUT_SECONDS = 3.0
20WIDTH_DEFAULT = 800
21HEIGHT_DEFAULT = 800
24def set_max_img_views_before_warning(new_value):
25 """Set the number of open views which triggers a warning.
27 If `None` or a negative number, disable the memory warning.
28 """
29 global MAX_IMG_VIEWS_BEFORE_WARNING
30 MAX_IMG_VIEWS_BEFORE_WARNING = new_value
33def _open_in_browser(content):
34 """Open a page in the user's web browser.
36 This function starts a local server in a separate thread, opens the page
37 with webbrowser, and shuts down the server once it has served one request.
38 """
39 queue = Queue()
41 class Handler(BaseHTTPRequestHandler):
42 def log_message(self, *args):
43 del args
45 def do_GET(self): # noqa: N802
46 if not self.path.endswith("index.html"):
47 self.send_error(HTTPStatus.NOT_FOUND, "File not found")
48 return
49 self.send_response(HTTPStatus.OK)
50 self.send_header("Content-type", "text/html")
51 self.send_header("Content-Length", str(len(content)))
52 self.end_headers()
53 self.wfile.write(content)
54 queue.put("done")
56 server = TCPServer(("", 0), Handler)
57 _, port = server.server_address
59 server_thread = Thread(target=server.serve_forever, daemon=True)
60 server_thread.start()
62 url = f"http://localhost:{port}/index.html"
63 webbrowser.open(url)
64 try:
65 queue.get(timeout=BROWSER_TIMEOUT_SECONDS)
66 except Empty:
67 raise RuntimeError(
68 "Failed to open nilearn plot or report in a web browser."
69 )
70 server.shutdown()
71 server_thread.join()
74class HTMLDocument:
75 """Embeds a plot in a web page.
77 If you are running a Jupyter notebook, the plot will be displayed
78 inline if this object is the output of a cell.
79 Otherwise, use ``open_in_browser()`` to open it in a web browser (or
80 ``save_as_html("filename.html")`` to save it as an html file).
82 Use ``str(document)`` or ``document.html`` to get the content of the
83 web page, and ``document.get_iframe()`` to have it wrapped in an iframe.
85 """
87 _all_open_html_repr: weakref.WeakSet = weakref.WeakSet()
89 def __init__(self, html, width=WIDTH_DEFAULT, height=HEIGHT_DEFAULT):
90 self.html = html
91 self.width = width
92 self.height = height
93 self._temp_file = None
94 self._check_n_open()
95 self._temp_file_removing_proc = None
97 @property
98 def width(self):
99 return self._width
101 @width.setter
102 def width(self, value):
103 try:
104 value = int(value)
105 except ValueError:
106 warnings.warn(
107 f"Invalid width {value=}. "
108 f"Using default instead {WIDTH_DEFAULT}",
109 stacklevel=find_stack_level(),
110 )
111 value = WIDTH_DEFAULT
113 self._width = value
115 @property
116 def height(self):
117 return self._height
119 @height.setter
120 def height(self, value):
121 try:
122 value = int(value)
123 except ValueError:
124 warnings.warn(
125 f"Invalid height {value=}. "
126 f"Using default instead {HEIGHT_DEFAULT}",
127 stacklevel=find_stack_level(),
128 )
129 value = WIDTH_DEFAULT
130 self._height = value
132 def _check_n_open(self):
133 HTMLDocument._all_open_html_repr.add(self)
134 if MAX_IMG_VIEWS_BEFORE_WARNING is None:
135 return
136 if MAX_IMG_VIEWS_BEFORE_WARNING < 0:
137 return
138 if (
139 len(HTMLDocument._all_open_html_repr)
140 > MAX_IMG_VIEWS_BEFORE_WARNING - 1
141 ):
142 warnings.warn(
143 "It seems you have created "
144 f"more than {MAX_IMG_VIEWS_BEFORE_WARNING} "
145 "nilearn views. As each view uses dozens "
146 "of megabytes of RAM, you might want to "
147 "delete some of them.",
148 stacklevel=find_stack_level(),
149 )
151 def resize(self, width, height):
152 """Resize the document displayed.
154 Parameters
155 ----------
156 width : :obj:`int`
157 New width of the document.
159 height : :obj:`int`
160 New height of the document.
162 """
163 self.width = width
164 self.height = height
165 return self
167 def get_iframe(self, width=None, height=None):
168 """Get the document wrapped in an inline frame.
170 For inserting in another HTML page of for display in a Jupyter
171 notebook.
173 Parameters
174 ----------
175 width : :obj:`int` or ``None``, default=None
176 Width of the inline frame.
178 height : :obj:`int` or ``None``, default=None
179 Height of the inline frame.
181 Returns
182 -------
183 wrapped : :obj:`str`
184 Raw HTML code for the inline frame.
186 """
187 if width is None:
188 width = self.width
189 if height is None:
190 height = self.height
191 escaped = escape(self.html, quote=True)
192 wrapped = (
193 f'<iframe srcdoc="{escaped}" '
194 f'width="{width}" height="{height}" '
195 'frameBorder="0"></iframe>'
196 )
197 return wrapped
199 def get_standalone(self):
200 """Return the plot in an HTML page."""
201 return self.html
203 def _repr_html_(self):
204 """Return html representation of the plot.
206 Used by the Jupyter notebook.
208 Users normally won't call this method explicitly.
210 See the jupyter documentation:
211 https://ipython.readthedocs.io/en/stable/config/integrating.html
212 """
213 return self.get_iframe()
215 def _repr_mimebundle_(self, include=None, exclude=None):
216 """Return html representation of the plot.
218 Used by the Jupyter notebook.
220 Users normally won't call this method explicitly.
222 See the jupyter documentation:
223 https://ipython.readthedocs.io/en/stable/config/integrating.html
224 """
225 del include, exclude
226 return {"text/html": self.get_iframe()}
228 def __str__(self):
229 return self.html
231 def save_as_html(self, file_name):
232 """Save the plot in an HTML file, that can later be opened \
233 in a browser.
235 Parameters
236 ----------
237 file_name : :obj:`str`
238 Path to the HTML file used for saving.
240 """
241 with Path(file_name).open("wb") as f:
242 f.write(self.get_standalone().encode("utf-8"))
244 @remove_parameters(
245 removed_params=["temp_file_lifetime"],
246 reason=(
247 "this function does not use a temporary file anymore "
248 "and 'temp_file_lifetime' has no effect."
249 ),
250 end_version="0.13.0",
251 )
252 def open_in_browser(
253 self,
254 file_name=None,
255 temp_file_lifetime="deprecated", # noqa: ARG002
256 ):
257 """Save the plot to a temporary HTML file and open it in a browser.
259 Parameters
260 ----------
261 file_name : :obj:`str` or ``None``, default=None
262 HTML file to use as a temporary file.
264 temp_file_lifetime : :obj:`float`, default=30
266 .. deprecated:: 0.10.3
268 The parameter is kept for backward compatibility and will be
269 removed in a future version. It has no effect.
270 """
271 if file_name is None:
272 _open_in_browser(self.get_standalone().encode("utf-8"))
273 else:
274 self.save_as_html(file_name)
275 webbrowser.open(f"file://{file_name}")