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
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-16 12:32 +0200
1"""Brain schematics plotting for glass brain functionality."""
3import json
4import pathlib
6from matplotlib import colors, patches, transforms
7from matplotlib.path import Path
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]
18def _codes_segment(pts): # noqa: ARG001
19 # pts is needed for API consistency with _codes_bezier
20 return [Path.MOVETO, Path.LINETO]
23def _codes(atype, pts):
24 dispatch = {"bezier": _codes_bezier, "segment": _codes_segment}
26 return dispatch[atype](pts)
29def _invert_color(color):
30 """Return inverted color.
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.
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
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"]
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 )
77 mpl_patches.append(patch)
79 return mpl_patches
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 }
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 }
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 }
108 direction_to_transforms = {
109 _direction: transforms.Affine2D.from_values(*params)
110 for _direction, params in direction_to_transform_params.items()
111 }
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 }
121 filename_and_transform = direction_to_json_and_transform.get(direction)
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)
131 return filename_and_transform
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))
139 xmin, xmax = min(x0, x1), max(x0, x1)
140 ymin, ymax = min(y0, y1), max(y0, y1)
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
149def plot_brain_schematics(ax, direction, **kwargs):
150 """Create matplotlib patches from a json custom format and plot them \
151 on a matplotlib Axes.
153 Parameters
154 ----------
155 ax : A MPL axes instance
156 The axes in which the plots will be drawn.
158 direction : {'x', 'y', 'z', 'l', 'r'}
159 The directions of the view.
161 **kwargs :
162 Passed to the matplotlib patches constructor.
164 Returns
165 -------
166 object_bounds : (xmin, xmax, ymin, ymax) tuple
167 Useful for the caller to be able to set axes limits.
169 """
170 get_axis_bg_color = ax.get_facecolor()
172 black_bg = colors.colorConverter.to_rgba(
173 get_axis_bg_color
174 ) == colors.colorConverter.to_rgba("k")
176 json_filename, transform = _get_json_and_transform(direction)
177 with json_filename.open() as json_file:
178 json_content = json.load(json_file)
180 mpl_patches = _get_mpl_patches(
181 json_content,
182 transform=transform + ax.transData,
183 invert_color=black_bg,
184 **kwargs,
185 )
187 for mpl_patch in mpl_patches:
188 ax.add_patch(mpl_patch)
190 object_bounds = _get_object_bounds(json_content, transform)
192 return object_bounds