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

73 statements  

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

1"""Brain schematics plotting for glass brain functionality.""" 

2 

3import json 

4import pathlib 

5 

6from matplotlib import colors, patches, transforms 

7from matplotlib.path import Path 

8 

9 

10def _codes_bezier(pts): 

11 bezier_num = len(pts) 

12 # Next two lines are meant to handle both Bezier 3 and 4 

13 path_attr = f"CURVE{bezier_num}" 

14 codes = [getattr(Path, path_attr)] * (bezier_num - 1) 

15 return [Path.MOVETO, *codes] 

16 

17 

18def _codes_segment(pts): # noqa: ARG001 

19 # pts is needed for API consistency with _codes_bezier 

20 return [Path.MOVETO, Path.LINETO] 

21 

22 

23def _codes(atype, pts): 

24 dispatch = {"bezier": _codes_bezier, "segment": _codes_segment} 

25 

26 return dispatch[atype](pts) 

27 

28 

29def _invert_color(color): 

30 """Return inverted color. 

31 

32 If color is (R, G, B) it returns (1 - R, 1 - G, 1 - B). If 

33 'color' can not be converted to a color it is returned 

34 unmodified. 

35 

36 """ 

37 try: 

38 color_converter = colors.ColorConverter() 

39 color_rgb = color_converter.to_rgb(color) 

40 return tuple(1 - level for level in color_rgb) 

41 except ValueError: 

42 return color 

43 

44 

45def _get_mpl_patches( 

46 json_content, transform=None, invert_color=False, **kwargs 

47): 

48 """Walk over the json content and build a list of matplotlib patches.""" 

49 mpl_patches = [] 

50 kwargs_edgecolor = kwargs.pop("edgecolor", None) 

51 kwargs_linewidth = kwargs.pop("linewidth", None) 

52 for path in json_content["paths"]: 

53 if kwargs_edgecolor is not None: 

54 edgecolor = kwargs_edgecolor 

55 else: 

56 edgecolor = path["edgecolor"] 

57 if invert_color: 

58 edgecolor = _invert_color(edgecolor) 

59 linewidth = kwargs_linewidth or path["linewidth"] 

60 path_id = path["id"] 

61 

62 for item in path["items"]: 

63 type = item["type"] 

64 pts = item["pts"] 

65 codes = _codes(type, pts) 

66 path = Path(pts, codes) 

67 patch = patches.PathPatch( 

68 path, 

69 edgecolor=edgecolor, 

70 linewidth=linewidth, 

71 facecolor="none", 

72 gid=path_id, 

73 transform=transform, 

74 **kwargs, 

75 ) 

76 

77 mpl_patches.append(patch) 

78 

79 return mpl_patches 

80 

81 

82def _get_json_and_transform(direction): 

83 """Return the json filename and an affine transform, which has \ 

84 been tweaked by hand to fit the MNI template. 

85 """ 

86 direction_to_view_name = { 

87 "x": "side", 

88 "y": "back", 

89 "z": "top", 

90 "l": "side", 

91 "r": "side", 

92 } 

93 

94 direction_to_transform_params = { 

95 "x": [0.38, 0, 0, 0.38, -108, -70], 

96 "y": [0.39, 0, 0, 0.39, -73, -73], 

97 "z": [0.36, 0, 0, 0.37, -71, -107], 

98 "l": [0.38, 0, 0, 0.38, -108, -70], 

99 "r": [0.38, 0, 0, 0.38, -108, -70], 

100 } 

101 

102 dirname = pathlib.Path(__file__).resolve().parent / "glass_brain_files" 

103 direction_to_filename = { 

104 _direction: dirname / f"brain_schematics_{view_name}.json" 

105 for _direction, view_name in direction_to_view_name.items() 

106 } 

107 

108 direction_to_transforms = { 

109 _direction: transforms.Affine2D.from_values(*params) 

110 for _direction, params in direction_to_transform_params.items() 

111 } 

112 

113 direction_to_json_and_transform = { 

114 _direction: ( 

115 direction_to_filename[_direction], 

116 direction_to_transforms[_direction], 

117 ) 

118 for _direction in direction_to_filename 

119 } 

120 

121 filename_and_transform = direction_to_json_and_transform.get(direction) 

122 

123 if filename_and_transform is None: 

124 message = ( 

125 f"No glass brain view associated with direction '{direction}'. " 

126 "Possible directions are " 

127 f"{list(direction_to_json_and_transform.keys())}" 

128 ) 

129 raise ValueError(message) 

130 

131 return filename_and_transform 

132 

133 

134def _get_object_bounds(json_content, transform): 

135 xmin, xmax, ymin, ymax = json_content["metadata"]["bounds"] 

136 x0, y0 = transform.transform((xmin, ymin)) 

137 x1, y1 = transform.transform((xmax, ymax)) 

138 

139 xmin, xmax = min(x0, x1), max(x0, x1) 

140 ymin, ymax = min(y0, y1), max(y0, y1) 

141 

142 # A combination of a proportional factor (fraction of the drawing) 

143 # and a guestimate of the linewidth 

144 xmargin = (xmax - xmin) * 0.025 + 0.1 

145 ymargin = (ymax - ymin) * 0.025 + 0.1 

146 return xmin - xmargin, xmax + xmargin, ymin - ymargin, ymax + ymargin 

147 

148 

149def plot_brain_schematics(ax, direction, **kwargs): 

150 """Create matplotlib patches from a json custom format and plot them \ 

151 on a matplotlib Axes. 

152 

153 Parameters 

154 ---------- 

155 ax : A MPL axes instance 

156 The axes in which the plots will be drawn. 

157 

158 direction : {'x', 'y', 'z', 'l', 'r'} 

159 The directions of the view. 

160 

161 **kwargs : 

162 Passed to the matplotlib patches constructor. 

163 

164 Returns 

165 ------- 

166 object_bounds : (xmin, xmax, ymin, ymax) tuple 

167 Useful for the caller to be able to set axes limits. 

168 

169 """ 

170 get_axis_bg_color = ax.get_facecolor() 

171 

172 black_bg = colors.colorConverter.to_rgba( 

173 get_axis_bg_color 

174 ) == colors.colorConverter.to_rgba("k") 

175 

176 json_filename, transform = _get_json_and_transform(direction) 

177 with json_filename.open() as json_file: 

178 json_content = json.load(json_file) 

179 

180 mpl_patches = _get_mpl_patches( 

181 json_content, 

182 transform=transform + ax.transData, 

183 invert_color=black_bg, 

184 **kwargs, 

185 ) 

186 

187 for mpl_patch in mpl_patches: 

188 ax.add_patch(mpl_patch) 

189 

190 object_bounds = _get_object_bounds(json_content, transform) 

191 

192 return object_bounds