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

1import base64 

2import re 

3 

4import numpy as np 

5import pytest 

6 

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 

18 

19try: 

20 from lxml import etree 

21 

22 LXML_INSTALLED = True 

23except ImportError: 

24 LXML_INSTALLED = False 

25 

26 

27def _normalize_ws(text): 

28 return re.sub(r"\s+", " ", text) 

29 

30 

31def test_add_js_lib(): 

32 """Tests for function add_js_lib. 

33 

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 

62 

63 

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 

74 

75 

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 

85 

86 

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 ) 

96 

97 

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

109 

110 

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

119 

120 

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) 

129 

130 

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 ] 

163 

164 

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) 

174 

175 

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

186 

187 

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" 

193 

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 

201 

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 

237 

238 

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 

257 

258 

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 )