yabplot 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
yabplot/scene.py ADDED
@@ -0,0 +1,124 @@
1
+ import os
2
+ import numpy as np
3
+ import pyvista as pv
4
+
5
+ def get_shading_preset(style_name):
6
+ """
7
+ Returns a dictionary of lighting parameters for pyvista.add_mesh.
8
+
9
+ Styles:
10
+ - 'default': Balanced, no shine.
11
+ - 'matte': (Soft) High ambient, low contrast. Good for reading atlas colors.
12
+ - 'sculpted':(Hard) Stronger shadows, higher contrast. Good for showing anatomy.
13
+ - 'glossy': (Shiny) Wet/Plastic look with specular highlights.
14
+ """
15
+ presets = {
16
+ 'default': {
17
+ 'lighting': True,
18
+ 'specular': 0.0,
19
+ 'ambient': 0.65,
20
+ 'diffuse': 0.4,
21
+ 'specular_power': 15
22
+ },
23
+ # very bright shadows
24
+ 'matte': {
25
+ 'lighting': True,
26
+ 'specular': 0.0,
27
+ 'ambient': 0.75,
28
+ 'diffuse': 0.2,
29
+ 'specular_power': 0
30
+ },
31
+ # slight shime, dark shadows, strong directional light
32
+ 'sculpted': {
33
+ 'lighting': True,
34
+ 'specular': 0.05,
35
+ 'ambient': 0.4,
36
+ 'diffuse': 0.6,
37
+ 'specular_power': 10
38
+ },
39
+ # strong shine, sharp highlights
40
+ 'glossy': {
41
+ 'lighting': True,
42
+ 'specular': 0.3,
43
+ 'ambient': 0.4,
44
+ 'diffuse': 0.6,
45
+ 'specular_power': 30
46
+ },
47
+ # flat 2D
48
+ 'flat': {
49
+ 'lighting': False,
50
+ 'ambient': 1.0,
51
+ 'diffuse': 0.0,
52
+ 'specular': 0.0
53
+ }
54
+ }
55
+
56
+ if style_name not in presets:
57
+ print(f"Warning: Style '{style_name}' not found. Using 'default'. Options: {list(presets.keys())}")
58
+ return presets['default']
59
+
60
+ return presets[style_name]
61
+
62
+ def get_view_configs(view_names):
63
+ all_views = {
64
+ 'left_lateral': {'pos': (-1, 0, 0), 'up': (0, 0, 1), 'side': 'L'},
65
+ 'right_lateral': {'pos': (1, 0, 0), 'up': (0, 0, 1), 'side': 'R'},
66
+ 'left_medial': {'pos': (1, 0, 0), 'up': (0, 0, 1), 'side': 'L'},
67
+ 'right_medial': {'pos': (-1, 0, 0), 'up': (0, 0, 1), 'side': 'R'},
68
+ 'superior': {'pos': (0, 0, 1), 'up': (0, 1, 0), 'side': 'both'},
69
+ 'inferior': {'pos': (0, 0, -1), 'up': (0, 1, 0), 'side': 'both'},
70
+ 'anterior': {'pos': (0, 1, 0), 'up': (0, 0, 1), 'side': 'both'},
71
+ 'posterior': {'pos': (0, -1, 0), 'up': (0, 0, 1), 'side': 'both'}
72
+ }
73
+ if view_names is None: return all_views
74
+ return {k: all_views[k] for k in view_names if k in all_views}
75
+
76
+ def setup_plotter(sel_views, layout, figsize, display_type, needs_bottom_row=True):
77
+ n = len(sel_views)
78
+ if layout is None:
79
+ if n <= 4: base_layout = (1, n)
80
+ elif n <= 6: base_layout = (2, 3)
81
+ else: base_layout = (int(np.ceil(n/4)), 4)
82
+ else: base_layout = layout
83
+
84
+ if needs_bottom_row:
85
+ nrows, ncols = base_layout[0] + 1, base_layout[1]
86
+ groups = [(nrows - 1, slice(0, ncols))]
87
+ row_weights = [1.0]*base_layout[0] + [0.2]
88
+ else:
89
+ nrows, ncols = base_layout[0], base_layout[1]
90
+ groups = None
91
+ row_weights = None
92
+
93
+ plotter = pv.Plotter(shape=(nrows, ncols), groups=groups, row_weights=row_weights,
94
+ off_screen=(display_type=='none'), window_size=figsize, border=False)
95
+ plotter.set_background('white')
96
+ return plotter, ncols, nrows
97
+
98
+ def add_context_to_view(plotter, bmesh, view_side, alpha, color, **kwargs):
99
+ """
100
+ Adds context mesh. Lighting parameters are passed via **kwargs.
101
+ """
102
+ if not bmesh: return
103
+ for h, mesh in bmesh.items():
104
+ if (view_side == 'L' and h == 'rh') or (view_side == 'R' and h == 'lh'): continue
105
+ plotter.add_mesh(mesh, color=color, opacity=alpha,
106
+ smooth_shading=True, show_edges=False,
107
+ **kwargs)
108
+
109
+ def set_camera(plotter, view_cfg, zoom=1.0, distance=200):
110
+ plotter.camera.position = tuple(p * distance for p in view_cfg['pos'])
111
+ plotter.camera.focal_point = (0, 0, 0)
112
+ plotter.camera.up = view_cfg['up']
113
+ plotter.camera.parallel_projection = True
114
+ plotter.reset_camera()
115
+ plotter.camera.zoom(zoom)
116
+
117
+ def finalize_plot(plotter, export_path, display_type):
118
+ if export_path: plotter.screenshot(export_path, transparent_background=True)
119
+
120
+ if display_type == 'static': plotter.show(jupyter_backend='static')
121
+ elif display_type == 'interactive': plotter.show(jupyter_backend='trame')
122
+ elif display_type == 'none': plotter.close()
123
+ return plotter
124
+
yabplot/utils.py ADDED
@@ -0,0 +1,170 @@
1
+ import os
2
+ import numpy as np
3
+ import pandas as pd
4
+ import nibabel as nib
5
+ import pyvista as pv
6
+ import matplotlib.pyplot as plt
7
+ from importlib.resources import files
8
+
9
+ def load_gii(gii_path):
10
+ """Load GIfTI geometry (vertices, faces)."""
11
+ mesh = nib.load(gii_path)
12
+ verts = mesh.darrays[0].data
13
+ faces = mesh.darrays[1].data
14
+ return verts, faces
15
+
16
+ def load_gii2pv(gii_path, smooth_i=0, smooth_f=0.1):
17
+ """
18
+ Load GIfTI and convert to PyVista format with optional smoothing.
19
+
20
+ Parameters
21
+ ----------
22
+ smooth_i : int
23
+ Number of smoothing iterations (e.g. 15).
24
+ smooth_f : float
25
+ Relaxation factor (0.0 to 1.0, e.g. 0.6).
26
+ """
27
+ verts, faces = load_gii(gii_path)
28
+
29
+ # create pyvista mesh
30
+ faces_pv = np.hstack([np.full((faces.shape[0], 1), 3), faces]).flatten().astype(int)
31
+ mesh = pv.PolyData(verts, faces_pv)
32
+
33
+ # apply smoothing
34
+ if smooth_i > 0:
35
+ # use Laplacian smoothing (standard vtkSmoothPolyDataFilter)
36
+ # note: higher relaxation factors can shrink the mesh significantly
37
+ # if shrinkage is an issue, could consider mesh.smooth_taubin() instead
38
+ mesh = mesh.smooth(n_iter=smooth_i, relaxation_factor=smooth_f)
39
+
40
+ return mesh
41
+
42
+ def make_cortical_mesh(verts, faces, scalars):
43
+ """Helper to create a PyVista mesh from raw buffers."""
44
+ faces_pv = np.hstack([np.full((faces.shape[0], 1), 3), faces]).flatten().astype(int)
45
+ mesh = pv.PolyData(verts, faces_pv)
46
+ mesh['Data'] = scalars
47
+ return mesh
48
+
49
+ def prep_data(data, regions, atlas, category):
50
+ """Standardize input data to dictionary."""
51
+ if isinstance(data, pd.DataFrame):
52
+ if data.shape[1] >= 2:
53
+ return dict(zip(data.iloc[:, 0], data.iloc[:, 1]))
54
+ elif isinstance(data, pd.Series):
55
+ return data.to_dict()
56
+ elif isinstance(data, dict):
57
+ return data
58
+ elif isinstance(data, (list, np.ndarray, tuple)):
59
+ if len(data) != len(regions):
60
+ raise ValueError(
61
+ f"Data length mismatch! Atlas '{atlas}' has {len(regions)} regions, "
62
+ f"but input data has {len(data)}. "
63
+ f"For partial data, use a dictionary, pd.Series, or pd.DataFrame. "
64
+ f"Use `yabplot.get_atlas_regions('{atlas}', '{category}')` to see expected order."
65
+ )
66
+ # map strictly by order
67
+ return dict(zip(regions, data))
68
+
69
+ return data
70
+
71
+ def generate_distinct_colors(n_colors, seed=42):
72
+ """Generate visually distinct colors using Golden Ratio."""
73
+ np.random.seed(seed)
74
+ colors = []
75
+ hue = np.random.rand()
76
+ for _ in range(n_colors):
77
+ hue = (hue + 0.618033988749895) % 1.0
78
+ colors.append(plt.cm.hsv(hue)[:3])
79
+ return colors
80
+
81
+ def parse_lut(lut_path):
82
+ """parses LUT to color array and name list."""
83
+
84
+ # load and sort by ID to ensure strict order (1..N)
85
+ df = pd.read_csv(lut_path, sep=r'\s+', header=None)
86
+ df = df.sort_values(by=0)
87
+
88
+ ids = df[0].values
89
+ names = df[1].tolist()
90
+ rgb = df.iloc[:, 2:5].values / 255.0
91
+
92
+ max_id = ids.max()
93
+
94
+ lut_colors = np.full((max_id + 1, 3), 0.5)
95
+ lut_names_list = ["Unknown"] * (max_id + 1)
96
+
97
+ lut_colors[ids] = rgb
98
+ for idx, name in zip(ids, names):
99
+ lut_names_list[idx] = name
100
+
101
+ return ids, lut_colors, lut_names_list, max_id
102
+
103
+ def map_values_to_surface(data, target_labels, lut_ids, dense_lut_names):
104
+ """maps data to vertices."""
105
+ # filter valid regions
106
+ valid_ids_list = []
107
+ valid_names_list = []
108
+
109
+ for rid in lut_ids:
110
+ if rid < len(dense_lut_names):
111
+ valid_ids_list.append(rid)
112
+ valid_names_list.append(dense_lut_names[rid])
113
+
114
+ valid_ids = np.array(valid_ids_list)
115
+ n_regions = len(valid_ids)
116
+
117
+ # atlas visualization without data
118
+ if data is None:
119
+ return target_labels
120
+
121
+ # data mapping
122
+ max_id = max(target_labels.max(), lut_ids.max())
123
+ lookup_table = np.full(max_id + 1, np.nan)
124
+ source_values = np.full(n_regions, np.nan)
125
+
126
+ if isinstance(data, dict):
127
+ for i, name in enumerate(valid_names_list):
128
+ if name in data:
129
+ source_values[i] = data[name]
130
+ elif isinstance(data, (np.ndarray, list, tuple)):
131
+ # map by order
132
+ if len(data) != n_regions:
133
+ raise ValueError(
134
+ f"Data length mismatch! The atlas LUT defines {n_regions} regions, "
135
+ f"but input data has {len(data)}.\n"
136
+ f"Expected order starts with: {valid_names_list[0:3]}...\n"
137
+ f"Solution: Use a dictionary for partial data, or check `yabplot.get_atlas_regions`."
138
+ )
139
+ source_values = np.array(data)
140
+ else:
141
+ raise ValueError("Data must be dict, list, or numpy array.")
142
+
143
+ lookup_table[valid_ids] = source_values
144
+ return lookup_table[target_labels]
145
+
146
+ def lines_from_streamlines(streamlines):
147
+ if len(streamlines) == 0: return np.array([]), np.array([]), np.array([])
148
+
149
+ points = np.vstack(streamlines)
150
+ n_points = [len(s) for s in streamlines]
151
+ offsets = np.insert(np.cumsum(n_points), 0, 0)[:-1]
152
+
153
+ cells = []
154
+ for length, offset in zip(n_points, offsets):
155
+ cells.append(np.hstack([[length], np.arange(offset, offset + length)]))
156
+ lines = np.hstack(cells)
157
+
158
+ # Calculate tangents
159
+ tangents = []
160
+ for s in streamlines:
161
+ if len(s) < 2:
162
+ tangents.append(np.array([[0,0,0]]))
163
+ continue
164
+ vecs = np.diff(s, axis=0)
165
+ vecs = np.vstack([vecs, vecs[-1:]])
166
+ norms = np.linalg.norm(vecs, axis=1, keepdims=True)
167
+ norms[norms == 0] = 1
168
+ tangents.append(vecs / norms)
169
+
170
+ return points, lines, np.vstack(tangents)
@@ -0,0 +1,97 @@
1
+ Metadata-Version: 2.4
2
+ Name: yabplot
3
+ Version: 0.1.0
4
+ Summary: yet another brain plot
5
+ Requires-Python: >=3.10
6
+ Description-Content-Type: text/markdown
7
+ License-File: LICENSE
8
+ Requires-Dist: ipywidgets>=8.1.8
9
+ Requires-Dist: nibabel>=5.3.2
10
+ Requires-Dist: pandas>=2.3.3
11
+ Requires-Dist: pooch>=1.8.2
12
+ Requires-Dist: pyvista>=0.46.4
13
+ Requires-Dist: scikit-image>=0.25.2
14
+ Requires-Dist: trame>=3.12.0
15
+ Requires-Dist: trame-vtk>=2.10.0
16
+ Requires-Dist: trame-vuetify>=3.1.0
17
+ Provides-Extra: docs
18
+ Requires-Dist: mkdocs; extra == "docs"
19
+ Requires-Dist: mkdocs-jupyter>=0.25.1; extra == "docs"
20
+ Requires-Dist: mkdocs-material; extra == "docs"
21
+ Requires-Dist: mkdocstrings[python]; extra == "docs"
22
+ Dynamic: license-file
23
+
24
+ # yabplot: yet another brain plot
25
+
26
+ ![logo](docs/assets/yabplot_logo.png)
27
+
28
+ [![PyPI version](https://img.shields.io/pypi/v/yabplot.svg)](https://pypi.org/project/yabplot/)
29
+ [![Docs](https://github.com/teanijarv/yabplot/actions/workflows/docs.yml/badge.svg)](https://teanijarv.github.io/yabplot/)
30
+ [![Tests](https://github.com/teanijarv/yabplot/actions/workflows/tests.yml/badge.svg)](https://github.com/teanijarv/yabplot/actions/workflows/tests.yml)
31
+ <!-- [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.XXXXXX.svg)](https://doi.org/10.5281/zenodo.XXXXXX) -->
32
+
33
+ **yabplot** is a Python library for creating beautiful, publication-quality 3D brain visualizations. it supports plotting cortical regions, subcortical structures, and white matter bundles.
34
+
35
+ the idea is simple. while there are already amazing visualization tools available, they often focus on specific domains—using one tool for white matter tracts and another for cortical surfaces inevitably leads to inconsistent styles. i wanted a unified, simple-to-use tool that enables me (and hopefully others) to perform most brain visualizations in a single place. recognizing that neuroscience evolves daily, i designed **yabplot** to be modular: it supports standard pre-packaged atlases out of the box, but easily accepts any custom parcellation or tractography dataset you might need.
36
+
37
+ ## features
38
+
39
+ * **pre-existing atlases:** access many commonly used atlases (schaefer2018, brainnetome, aparc, aseg, musus100, xtract, etc) on demand.
40
+ * **simple to use:** plug-n-play functions for cortex, subcortex, and tracts with a unified API.
41
+ * **custom atlases:** easily use your own parcellations, segmentations (.nii/.gii), or tractograms (.trk).
42
+ * **flexible inputs:** accepts data as dictionaries (for partial mapping) or arrays (for strict mapping).
43
+
44
+ ## installation
45
+
46
+ ```bash
47
+ uv add yabplot
48
+ ```
49
+ or
50
+ ```bash
51
+ pip install yabplot
52
+ ```
53
+
54
+ dependencies: python 3.11 with ipywidgets, nibabel, pandas, pooch, pyvista, scikit-image, trame, trame-vtk, trame-vuetify
55
+
56
+ ## quick start
57
+
58
+ please refer to the [documentation](https://teanijarv.github.io/yabplot/) for more comprehensive guides.
59
+
60
+ ```python
61
+ import yabplot as yab
62
+ import numpy as np
63
+
64
+ # see available cortical atlases
65
+ atlases = yab.get_available_resources(category='cortical')
66
+
67
+ # see the region names within the aseg atlas
68
+ regions = yab.get_atlas_regions(atlas='aseg', category='subcortical')
69
+
70
+ # plot data on cortical regions
71
+ data = np.arange(0, 1, 0.001)
72
+ yab.plot_cortical(data=data, atlas='schaefer_1000', figsize=(600, 300),
73
+ cmap='viridis', vminmax=[0, 1], style='default',
74
+ views=['left_lateral', 'superior', 'right_lateral'])
75
+
76
+
77
+ # plot values for specific subcortical regions
78
+ data = {'Left_Amygdala': 0.8, 'Right_Hippocampus': 0.5,
79
+ 'Right_Thalamus': -0.5, 'Left_Putamen': -1}
80
+ yab.plot_subcortical(data=data, atlas='aseg', figsize=(600, 450), layout=(2, 2),
81
+ views=['superior', 'anterior', 'left_lateral', 'right_lateral'],
82
+ cmap='coolwarm', vminmax=[-1, 1], style='matte')
83
+
84
+ # plot data on white matter bundles
85
+ regions = yab.get_atlas_regions(atlas='xtract_tiny', category='tracts')
86
+ data = np.arange(0, len(regions))
87
+ yab.plot_tracts(data=data, atlas='xtract_tiny', figsize=(600, 300),
88
+ views=['superior', 'anterior', 'left_lateral'], nan_color='#cccccc',
89
+ bmesh_type='fsaverage', style='default', cmap='plasma')
90
+
91
+ ```
92
+
93
+ ![examples](docs/assets/examples.png)
94
+
95
+ ## acknowledgements
96
+
97
+ yabplot relies on the extensive work of the neuroimaging community. if you use these atlases in your work, please cite the original authors.
@@ -0,0 +1,10 @@
1
+ yabplot/__init__.py,sha256=qWDPpGuPgMJhol88FJ1BlPgnu1kMQQWj-iLUAniUVJQ,308
2
+ yabplot/plotting.py,sha256=UI2fOqNeHIB0_Exj0bltnyUrEhpeNMl7cEJkS9Uzdws,25967
3
+ yabplot/scene.py,sha256=vQ4QxOdQ6uhoMGtz9dxQi5G0E5gip5DR3v3sbjMviaU,4522
4
+ yabplot/utils.py,sha256=ROtBqmTqmhNrdPBUCqp4hWmRv363i-R7xH1SZGHTNNk,5737
5
+ yabplot/data/__init__.py,sha256=jYR5iyJeEk0qRt8jYcaHGy6q6uJQVHFUasj78SH5X64,12047
6
+ yabplot-0.1.0.dist-info/licenses/LICENSE,sha256=bz__yccnNr-Tsjp1iTjk2KBb1EgW4mdkdZCLAU-Yw24,1063
7
+ yabplot-0.1.0.dist-info/METADATA,sha256=NmCvg1vkedOTV-fRdt6hFe7ZjNBsWhTkaOmn5srinS0,4331
8
+ yabplot-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
9
+ yabplot-0.1.0.dist-info/top_level.txt,sha256=hrhVrEs-yKkYrOJfuc84JCkuOxJMN2AxEU5jxA1tZco,8
10
+ yabplot-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2026 Toomas Erik Anijärv
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
14
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
15
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
16
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
17
+ DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
18
+ OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
19
+ OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1 @@
1
+ yabplot