Coverage for nilearn/plotting/tests/test_js_plotting_utils.py: 0%
141 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-16 12:32 +0200
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-16 12:32 +0200
1import base64
2import re
4import numpy as np
5import pytest
7from nilearn.datasets import fetch_surf_fsaverage
8from nilearn.plotting.js_plotting_utils import (
9 add_js_lib,
10 colorscale,
11 decode,
12 encode,
13 get_html_template,
14 mesh_to_plotly,
15 to_color_strings,
16)
17from nilearn.surface import load_surf_mesh
19try:
20 from lxml import etree
22 LXML_INSTALLED = True
23except ImportError:
24 LXML_INSTALLED = False
27def _normalize_ws(text):
28 return re.sub(r"\s+", " ", text)
31def test_add_js_lib():
32 """Tests for function add_js_lib.
34 Checks that the html page contains the javascript code.
35 """
36 html = get_html_template("surface_plot_template.html")
37 cdn = add_js_lib(html, embed_js=False)
38 assert "decodeBase64" in cdn
39 assert _normalize_ws(
40 """<script
41 src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js">
42 </script>
43 <script src="https://cdn.plot.ly/plotly-gl3d-latest.min.js"></script>
44 """
45 ) in _normalize_ws(cdn)
46 inline = _normalize_ws(add_js_lib(html, embed_js=True))
47 assert (
48 _normalize_ws(
49 """/*! jQuery v3.6.0 | (c) OpenJS Foundation and other
50 contributors | jquery.org/license */"""
51 )
52 in inline
53 )
54 assert (
55 _normalize_ws(
56 """**
57 * plotly.js (gl3d - minified)"""
58 )
59 in inline
60 )
61 assert "decodeBase64" in inline
64def check_colors(colors):
65 """Perform several checks on colors obtained from function colorscale."""
66 assert len(colors) == 100
67 val, cstring = zip(*colors)
68 assert np.allclose(np.linspace(0, 1, 100), val, atol=1e-3)
69 assert val[0] == 0
70 assert val[-1] == 1
71 for cs in cstring:
72 assert re.match(r"rgb\(\d+, \d+, \d+\)", cs)
73 return val, cstring
76def test_colorscale_no_threshold():
77 """Test colorscale with no thresholding."""
78 values = np.linspace(-13, -1.5, 20)
79 colors = colorscale("jet", values, None)
80 check_colors(colors["colors"])
81 assert (colors["vmin"], colors["vmax"]) == (-13, 13)
82 assert colors["cmap"].N == 256
83 assert (colors["norm"].vmax, colors["norm"].vmin) == (13, -13)
84 assert colors["abs_threshold"] is None
87@pytest.fixture
88def expected_abs_threshold(threshold):
89 """Return the expected absolute threshold."""
90 expected = {"0%": 1.5, "50%": 7.55, "99%": 13}
91 return (
92 expected.get(threshold)
93 if isinstance(threshold, str)
94 else abs(threshold)
95 )
98@pytest.mark.parametrize("threshold", ["0%", "50%", "99%", 0.5, 7.25])
99def test_colorscale_threshold(threshold, expected_abs_threshold):
100 """Test colorscale with different threshold values."""
101 colors = colorscale("jet", np.linspace(-13, -1.5, 20), threshold=threshold)
102 _, cstring = check_colors(colors["colors"])
103 assert cstring[50] == "rgb(127, 127, 127)"
104 assert (colors["vmin"], colors["vmax"]) == (-13, 13)
105 assert colors["cmap"].N == 256
106 assert (colors["norm"].vmax, colors["norm"].vmin) == (13, -13)
107 assert np.allclose(colors["abs_threshold"], expected_abs_threshold, 2)
108 assert colors["symmetric_cmap"]
111@pytest.mark.parametrize("vmin,vmax", [(None, 7), (-5, 7)])
112def test_colorscale_symmetric_cmap(vmin, vmax):
113 """Test colorscale with symmetric cmap and positive values."""
114 colors = colorscale("jet", np.arange(15), vmin=vmin, vmax=vmax)
115 assert (colors["vmin"], colors["vmax"]) == (-7, 7)
116 assert colors["cmap"].N == 256
117 assert (colors["norm"].vmax, colors["norm"].vmin) == (7, -7)
118 assert colors["symmetric_cmap"]
121@pytest.fixture
122def expected_vmin_vmax(values, vmax, vmin):
123 """Return expected vmin and vmax."""
124 if vmax is None:
125 return (min(values), max(values))
126 if min(values) < 0:
127 return (-vmax, vmax)
128 return (min(values), vmax) if vmin is None else (vmin, vmax)
131@pytest.mark.parametrize(
132 "values,vmax,vmin,threshold",
133 [
134 (np.arange(15), None, None, None),
135 (np.arange(15), 7, None, None),
136 (np.arange(15), 7, -5, None),
137 (np.arange(15) + 3, 7, None, None),
138 (np.arange(15) + 3, None, None, None),
139 (np.arange(15) + 3, 7, 1, None),
140 (np.arange(15) + 3, 10, 6, 5),
141 (np.arange(15) + 3, 10, None, 5),
142 (np.linspace(-15, 4), 7, None, None),
143 ],
144)
145def test_colorscale_asymmetric_cmap(
146 values, vmax, vmin, threshold, expected_vmin_vmax
147):
148 """Test colorscale with asymmetric cmap."""
149 colors = colorscale(
150 "jet",
151 values,
152 vmax=vmax,
153 vmin=vmin,
154 threshold=threshold,
155 symmetric_cmap=False,
156 )
157 assert (min(values) < 0) | (not colors["symmetric_cmap"])
158 assert colors["cmap"].N == 256
159 assert (int(colors["vmin"]), int(colors["vmax"])) == expected_vmin_vmax
160 assert (colors["norm"].vmax, colors["norm"].vmin) == expected_vmin_vmax[
161 ::-1
162 ]
165@pytest.mark.parametrize("dtype", ["<f4", "<i4", ">f4", ">i4"])
166def test_encode(dtype):
167 """Test base64 encoding/decoding for different dtypes."""
168 a = np.arange(10, dtype=dtype)
169 encoded = encode(a)
170 decoded = base64.b64decode(encoded.encode("utf-8"))
171 b = np.frombuffer(decoded, dtype=dtype)
172 assert np.allclose(decode(encoded, dtype=dtype), b)
173 assert np.allclose(a, b)
176@pytest.mark.parametrize("hemi", ["left", "right"])
177def test_mesh_to_plotly(hemi):
178 """Tests for function mesh_to_plotly."""
179 fsaverage = fetch_surf_fsaverage()
180 coord, triangles = load_surf_mesh(fsaverage[f"pial_{hemi}"])
181 plotly = mesh_to_plotly(fsaverage[f"pial_{hemi}"])
182 for i, key in enumerate(["_x", "_y", "_z"]):
183 assert np.allclose(decode(plotly[key], "<f4"), coord[:, i])
184 for i, key in enumerate(["_i", "_j", "_k"]):
185 assert np.allclose(decode(plotly[key], "<i4"), triangles[:, i])
188def check_html(
189 tmp_path, html, check_selects=True, plot_div_id="surface-plot", title=None
190):
191 """Perform several checks on raw HTML code."""
192 tmpfile = tmp_path / "test.html"
194 html.save_as_html(tmpfile)
195 with tmpfile.open() as f:
196 saved = f.read()
197 # If present, replace Windows line-end '\r\n' with Unix's '\n'
198 saved = saved.replace("\r\n", "\n")
199 standalone = html.get_standalone().replace("\r\n", "\n")
200 assert saved == standalone
202 assert "INSERT" not in html.html
203 assert html.get_standalone() == html.html
204 assert html._repr_html_() == html.get_iframe()
205 assert str(html) == html.get_standalone()
206 assert '<meta charset="UTF-8" />' in str(html)
207 resized = html.resize(3, 17)
208 assert resized is html
209 assert (html.width, html.height) == (3, 17)
210 assert 'width="3" height="17"' in html.get_iframe()
211 assert 'width="33" height="37"' in html.get_iframe(33, 37)
212 if title is not None:
213 assert f"<title>{title}</title>" in str(html)
214 if not LXML_INSTALLED:
215 return
216 root = etree.HTML(
217 html.html.encode("utf-8"), parser=etree.HTMLParser(huge_tree=True)
218 )
219 head = root.find("head")
220 assert len(head.findall("script")) == 5
221 body = root.find("body")
222 div = body.find("div")
223 assert ("id", plot_div_id) in div.items()
224 if not check_selects:
225 return
226 selects = body.findall("select")
227 assert len(selects) == 3
228 hemi = selects[0]
229 assert ("id", "select-hemisphere") in hemi.items()
230 assert len(hemi.findall("option")) == 3
231 kind = selects[1]
232 assert ("id", "select-kind") in kind.items()
233 assert len(kind.findall("option")) == 2
234 view = selects[2]
235 assert ("id", "select-view") in view.items()
236 assert len(view.findall("option")) == 7
239@pytest.mark.parametrize(
240 "colors",
241 [
242 [[0, 0, 1], [1, 0, 0], [0.5, 0.5, 0.5]],
243 [[0, 0, 1, 1], [1, 0, 0, 1], [0.5, 0.5, 0.5, 0]],
244 ["#0000ff", "#ff0000", "#7f7f7f"],
245 [[0, 0, 1, 1], [1, 0, 0, 1], [0.5, 0.5, 0.5, 0]],
246 ["r", "green", "black", "white"],
247 ["#0000ffff", "#ff0000ab", "#7f7f7f00"],
248 ],
249)
250def test_to_color_strings(colors):
251 """Tests for function to_color_strings with different color inputs."""
252 if len(colors) == 3:
253 expected = ["#0000ff", "#ff0000", "#7f7f7f"]
254 else:
255 expected = ["#ff0000", "#008000", "#000000", "#ffffff"]
256 assert to_color_strings(colors) == expected
259def test_import_html_document_from_js_plotting():
260 """Smoke test importing HTMLDocument from js_plotting_utils."""
261 from nilearn.plotting.js_plotting_utils import ( # noqa: F401
262 HTMLDocument,
263 set_max_img_views_before_warning,
264 )