Coverage for nilearn/plotting/cm.py: 0%

135 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-16 12:32 +0200

1"""Matplotlib colormaps useful for neuroimaging.""" 

2 

3import matplotlib 

4import numpy as _np 

5from matplotlib import cm as _cm 

6from matplotlib import colors as _colors 

7from matplotlib import rcParams as _rcParams 

8 

9from nilearn._utils import compare_version 

10 

11############################################################################### 

12# Custom colormaps for two-tailed symmetric statistics 

13 

14# mypy: disable_error_code="attr-defined" 

15 

16__all__ = ["_cmap_d"] 

17 

18 

19def mix_colormaps(fg, bg): 

20 """Mixes foreground and background arrays of RGBA colors. 

21 

22 Parameters 

23 ---------- 

24 fg : numpy.ndarray 

25 Array of shape (n, 4), foreground RGBA colors 

26 represented as floats in [0, 1] 

27 bg : numpy.ndarray 

28 Array of shape (n, 4), background RGBA colors 

29 represented as floats in [0, 1] 

30 

31 Returns 

32 ------- 

33 mix : numpy.ndarray 

34 Array of shape (n, 4), mixed colors 

35 represented as floats in [0, 1] 

36 """ 

37 # Adapted from https://stackoverflow.com/questions/726549/algorithm-for-additive-color-mixing-for-rgb-values/727339#727339 # noqa: E501 

38 if fg.shape != bg.shape: 

39 raise ValueError( 

40 "Trying to mix colormaps with different shapes: " 

41 f"{fg.shape}, {bg.shape}" 

42 ) 

43 

44 mix = _np.empty_like(fg) 

45 

46 mix[:, 3] = 1 - (1 - fg[:, 3]) * (1 - bg[:, 3]) 

47 

48 for color_index in range(3): 

49 mix[:, color_index] = ( 

50 fg[:, color_index] * fg[:, 3] 

51 + bg[:, color_index] * bg[:, 3] * (1 - fg[:, 3]) 

52 ) / mix[:, 3] 

53 

54 return mix 

55 

56 

57def _rotate_cmap(cmap, swap_order=("green", "red", "blue")): 

58 """Swap the colors of a colormap.""" 

59 cdict = cmap._segmentdata.copy() 

60 

61 return { 

62 "green": list(cdict[swap_order[0]]), 

63 "blue": list(cdict[swap_order[1]]), 

64 "red": list(cdict[swap_order[2]]), 

65 } 

66 

67 

68def _fill_pigtailed_cmap(cdict, channel1, channel2): 

69 return [ 

70 (0.5 * (1 - p), c1, c2) for (p, c1, c2) in reversed(cdict[channel1]) 

71 ] + [(0.5 * (1 + p), c1, c2) for (p, c1, c2) in cdict[channel2]] 

72 

73 

74def _pigtailed_cmap(cmap, swap_order=("green", "red", "blue")): 

75 """Make a new colormap by concatenating a colormap with its reverse.""" 

76 cdict = cmap._segmentdata.copy() 

77 

78 return { 

79 "green": _fill_pigtailed_cmap(cdict, swap_order[0], "green"), 

80 "blue": _fill_pigtailed_cmap(cdict, swap_order[1], "blue"), 

81 "red": _fill_pigtailed_cmap(cdict, swap_order[2], "red"), 

82 } 

83 

84 

85def _concat_cmap(cmap1, cmap2): 

86 """Make a new colormap by concatenating two colormaps.""" 

87 cdict = {} 

88 

89 cdict1 = cmap1._segmentdata.copy() 

90 cdict2 = cmap2._segmentdata.copy() 

91 if not callable(cdict1["red"]): 

92 for c in ["red", "green", "blue"]: 

93 cdict[c] = [(0.5 * p, c1, c2) for (p, c1, c2) in cdict1[c]] 

94 else: 

95 for c in ["red", "green", "blue"]: 

96 cdict[c] = [] 

97 ps = _np.linspace(0, 1, 10) 

98 colors = cmap1(ps) 

99 for p, (r, g, b, _) in zip(ps, colors): 

100 cdict["red"].append((0.5 * p, r, r)) 

101 cdict["green"].append((0.5 * p, g, g)) 

102 cdict["blue"].append((0.5 * p, b, b)) 

103 if not callable(cdict2["red"]): 

104 for c in ["red", "green", "blue"]: 

105 cdict[c].extend( 

106 [(0.5 * (1 + p), c1, c2) for (p, c1, c2) in cdict2[c]] 

107 ) 

108 else: 

109 ps = _np.linspace(0, 1, 10) 

110 colors = cmap2(ps) 

111 for p, (r, g, b, _) in zip(ps, colors): 

112 cdict["red"].append((0.5 * (1 + p), r, r)) 

113 cdict["green"].append((0.5 * (1 + p), g, g)) 

114 cdict["blue"].append((0.5 * (1 + p), b, b)) 

115 

116 return cdict 

117 

118 

119def alpha_cmap(color, name="", alpha_min=0.5, alpha_max=1.0): 

120 """Return a colormap with the given color, and alpha going from zero to 1. 

121 

122 Parameters 

123 ---------- 

124 color : (r, g, b), or a :obj:`str` 

125 A triplet of floats ranging from 0 to 1, or a matplotlib 

126 color string. 

127 

128 name : :obj:`str` , default='' 

129 Name of the colormap. 

130 

131 alpha_min : :obj:`float`, default=0.5 

132 Minimum value for alpha. 

133 

134 alpha_max : :obj:`float`, default=1.0 

135 Maximum value for alpha. 

136 

137 """ 

138 red, green, blue = _colors.colorConverter.to_rgb(color) 

139 if name == "" and hasattr(color, "startswith"): 

140 name = color 

141 cmapspec = [(red, green, blue, 1.0), (red, green, blue, 1.0)] 

142 cmap = _colors.LinearSegmentedColormap.from_list( 

143 f"{name}_transparent", cmapspec, _rcParams["image.lut"] 

144 ) 

145 cmap._init() 

146 cmap._lut[:, -1] = _np.linspace(alpha_min, alpha_max, cmap._lut.shape[0]) 

147 cmap._lut[-1, -1] = 0 

148 return cmap 

149 

150 

151############################################################################### 

152# Our colormaps definition 

153 

154 

155_cmaps_data = { 

156 "cold_hot": _pigtailed_cmap(_cm.hot), 

157 "cold_white_hot": _pigtailed_cmap(_cm.hot_r), 

158 "brown_blue": _pigtailed_cmap(_cm.bone), 

159 "cyan_copper": _pigtailed_cmap(_cm.copper), 

160 "cyan_orange": _pigtailed_cmap(_cm.YlOrBr_r), 

161 "blue_red": _pigtailed_cmap(_cm.Reds_r), 

162 "brown_cyan": _pigtailed_cmap(_cm.Blues_r), 

163 "purple_green": _pigtailed_cmap( 

164 _cm.Greens_r, swap_order=("red", "blue", "green") 

165 ), 

166 "purple_blue": _pigtailed_cmap( 

167 _cm.Blues_r, swap_order=("red", "blue", "green") 

168 ), 

169 "blue_orange": _pigtailed_cmap( 

170 _cm.Oranges_r, swap_order=("green", "red", "blue") 

171 ), 

172 "black_blue": _rotate_cmap(_cm.hot), 

173 "black_purple": _rotate_cmap(_cm.hot, swap_order=("blue", "red", "green")), 

174 "black_pink": _rotate_cmap(_cm.hot, swap_order=("blue", "green", "red")), 

175 "black_green": _rotate_cmap(_cm.hot, swap_order=("red", "blue", "green")), 

176 "black_red": _cm.hot._segmentdata.copy(), 

177} 

178 

179_cmaps_data["ocean_hot"] = _concat_cmap(_cm.ocean, _cm.hot_r) 

180_cmaps_data["hot_white_bone"] = _concat_cmap(_cm.afmhot, _cm.bone_r) 

181 

182_cmaps_data["hot_black_bone"] = _concat_cmap(_cm.afmhot_r, _cm.bone) 

183 

184 

185############################################################################### 

186# Build colormaps and their reverse. 

187 

188 

189# backported and adapted from matplotlib since it's deprecated in 3.2 

190def _revcmap(data): 

191 data_r = { 

192 key: [(1.0 - x, y1, y0) for x, y0, y1 in reversed(val)] 

193 for key, val in data.items() 

194 } 

195 return data_r 

196 

197 

198_cmap_d = {} 

199 

200for _cmapname in list(_cmaps_data.keys()): # needed as dict changes in loop 

201 _cmapname_r = f"{_cmapname}_r" 

202 _cmapspec = _cmaps_data[_cmapname] 

203 _cmaps_data[_cmapname_r] = _revcmap(_cmapspec) 

204 _cmap_d[_cmapname] = _colors.LinearSegmentedColormap( 

205 _cmapname, _cmapspec, _rcParams["image.lut"] 

206 ) 

207 _cmap_d[_cmapname_r] = _colors.LinearSegmentedColormap( 

208 _cmapname_r, _cmaps_data[_cmapname_r], _rcParams["image.lut"] 

209 ) 

210 

211############################################################################### 

212# A few transparent colormaps 

213for color, name in ( 

214 ((1, 0, 0), "red"), 

215 ((0, 1, 0), "green"), 

216 ((0, 0, 1), "blue"), 

217): 

218 _cmap_d[f"{name}_transparent"] = alpha_cmap(color, name=name) 

219 _cmap_d[f"{name}_transparent_full_alpha_range"] = alpha_cmap( 

220 color, alpha_min=0, alpha_max=1, name=name 

221 ) 

222 

223############################################################################### 

224# HCP Connectome Workbench colormaps 

225# As seen in https://github.com/Washington-University/workbench src/Palette 

226roy_big_bl = ( 

227 _np.array( 

228 [ 

229 (255, 255, 0), 

230 (255, 200, 0), 

231 (255, 120, 0), 

232 (255, 0, 0), 

233 (200, 0, 0), 

234 (150, 0, 0), 

235 (100, 0, 0), 

236 (60, 0, 0), 

237 (0, 0, 0), 

238 (0, 0, 80), 

239 (0, 0, 170), 

240 (75, 0, 125), 

241 (125, 0, 160), 

242 (75, 125, 0), 

243 (0, 200, 0), 

244 (0, 255, 0), 

245 (0, 255, 255), 

246 (0, 255, 255), 

247 ][::-1] 

248 ) 

249 / 255 

250) 

251 

252videen_style = [ 

253 "#000000", 

254 "#bbbbbb", 

255 "#dddddd", 

256 "#ffffff", 

257 "#ff388d", 

258 "#e251e2", 

259 "#10b010", 

260 "#00ff00", 

261 "#00ffff", 

262 "#000000", 

263 "#660033", 

264 "#33334c", 

265 "#4c4c7f", 

266 "#7f7fcc", 

267 "#00ff00", 

268 "#10b010", 

269 "#ffff00", 

270 "#ff9900", 

271 "#ff6900", 

272 "#ff0000", 

273] 

274 

275_cmap_d["roy_big_bl"] = _colors.LinearSegmentedColormap.from_list( 

276 "roy_big_bl", roy_big_bl.tolist() 

277) 

278_cmap_d["videen_style"] = _colors.LinearSegmentedColormap.from_list( 

279 "videen_style", videen_style 

280) 

281 

282# Save colormaps in the scope of the module 

283globals().update(_cmap_d) 

284# Register cmaps in matplotlib too 

285for k, v in _cmap_d.items(): 

286 if compare_version(matplotlib.__version__, ">=", "3.5.0"): 

287 from matplotlib import colormaps as _colormaps 

288 

289 _register_cmap = _colormaps.register 

290 else: 

291 _register_cmap = _cm.register_cmap 

292 

293 _register_cmap(name=k, cmap=v) 

294 

295 

296############################################################################### 

297# Utility to replace a colormap by another in an interval 

298 

299 

300def dim_cmap(cmap, factor=0.3, to_white=True): 

301 """Dim a colormap to white, or to black.""" 

302 assert 0 <= factor <= 1, ( 

303 "Dimming factor must be larger than 0 and smaller than 1, " 

304 f"{factor} was passed." 

305 ) 

306 if to_white: 

307 

308 def dimmer(c): 

309 return 1 - factor * (1 - c) 

310 

311 else: 

312 

313 def dimmer(c): 

314 return factor * c 

315 

316 cdict = cmap._segmentdata.copy() 

317 for _, color in enumerate(("red", "green", "blue")): 

318 color_lst = [] 

319 for value, c1, c2 in cdict[color]: 

320 color_lst.append((value, dimmer(c1), dimmer(c2))) 

321 cdict[color] = color_lst 

322 

323 return _colors.LinearSegmentedColormap( 

324 f"{cmap.name}_dimmed", cdict, _rcParams["image.lut"] 

325 ) 

326 

327 

328def replace_inside(outer_cmap, inner_cmap, vmin, vmax): 

329 """Replace a colormap by another inside a pair of values.""" 

330 assert vmin < vmax, ( 

331 f"'vmin' must be smaller than 'vmax'. Got {vmin=} and {vmax=}." 

332 ) 

333 assert vmin >= 0, f"'vmin' must be larger than 0, {vmin=} was passed." 

334 assert vmax <= 1, f"'vmax' must be smaller than 1, {vmax=} was passed." 

335 outer_cdict = outer_cmap._segmentdata.copy() 

336 inner_cdict = inner_cmap._segmentdata.copy() 

337 

338 cdict = {} 

339 for this_cdict, cmap in [ 

340 (outer_cdict, outer_cmap), 

341 (inner_cdict, inner_cmap), 

342 ]: 

343 if callable(this_cdict["red"]): 

344 ps = _np.linspace(0, 1, 25) 

345 colors = cmap(ps) 

346 this_cdict["red"] = [] 

347 this_cdict["green"] = [] 

348 this_cdict["blue"] = [] 

349 for p, (r, g, b, _) in zip(ps, colors): 

350 this_cdict["red"].append((p, r, r)) 

351 this_cdict["green"].append((p, g, g)) 

352 this_cdict["blue"].append((p, b, b)) 

353 

354 for c_index, color in enumerate(("red", "green", "blue")): 

355 color_lst = [] 

356 

357 for value, c1, c2 in outer_cdict[color]: 

358 if value >= vmin: 

359 break 

360 color_lst.append((value, c1, c2)) 

361 

362 color_lst.append( 

363 (vmin, outer_cmap(vmin)[c_index], inner_cmap(vmin)[c_index]) 

364 ) 

365 

366 for value, c1, c2 in inner_cdict[color]: 

367 if value <= vmin: 

368 continue 

369 if value >= vmax: 

370 break 

371 color_lst.append((value, c1, c2)) 

372 

373 color_lst.append( 

374 (vmax, inner_cmap(vmax)[c_index], outer_cmap(vmax)[c_index]) 

375 ) 

376 

377 color_lst.extend( 

378 (value, c1, c2) 

379 for value, c1, c2 in outer_cdict[color] 

380 if value > vmax 

381 ) 

382 cdict[color] = color_lst 

383 

384 return _colors.LinearSegmentedColormap( 

385 f"{inner_cmap.name}_inside_{outer_cmap.name}", 

386 cdict, 

387 _rcParams["image.lut"], 

388 )