bsplot 0.0.8__tar.gz → 0.0.9__tar.gz

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.
Files changed (88) hide show
  1. {bsplot-0.0.8 → bsplot-0.0.9}/PKG-INFO +2 -1
  2. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot/__init__.py +1 -1
  3. bsplot-0.0.9/bsplot/data/STN_lh.nii.gz +0 -0
  4. bsplot-0.0.9/bsplot/data/STN_rh.nii.gz +0 -0
  5. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot/graph/edges.py +5 -11
  6. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot/streamlines.py +3 -0
  7. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot/style.py +184 -28
  8. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot/styles/black.mplstyle +0 -1
  9. bsplot-0.0.9/bsplot/styles/template.mplstyle +83 -0
  10. bsplot-0.0.9/bsplot/styles/white.mplstyle +77 -0
  11. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot/surface.py +67 -4
  12. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot/templates.py +1 -0
  13. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot/text.py +1 -0
  14. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot.egg-info/SOURCES.txt +5 -1
  15. {bsplot-0.0.8 → bsplot-0.0.9}/pyproject.toml +3 -1
  16. {bsplot-0.0.8 → bsplot-0.0.9}/LICENSE +0 -0
  17. {bsplot-0.0.8 → bsplot-0.0.9}/MANIFEST.in +0 -0
  18. {bsplot-0.0.8 → bsplot-0.0.9}/README.md +0 -0
  19. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot/anat.py +0 -0
  20. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot/animate.py +0 -0
  21. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot/bioicons.py +0 -0
  22. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot/brain.py +0 -0
  23. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot/colors.py +0 -0
  24. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot/data/HCP_avg-SC.txt +0 -0
  25. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot/data/MNI152.rh.pial +0 -0
  26. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot/data/__init__.py +0 -0
  27. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot/data/parcellations/HCP-MMP1.L.label.gii +0 -0
  28. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot/data/parcellations/HCP-MMP1.R.label.gii +0 -0
  29. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot/data/surface.py +0 -0
  30. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot/figure.py +0 -0
  31. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot/graph/__init__.py +0 -0
  32. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot/graph/flowchart.py +0 -0
  33. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot/graph/layout.py +0 -0
  34. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot/graph/network.py +0 -0
  35. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot/graph/nodes.py +0 -0
  36. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot/panels.py +0 -0
  37. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot/scientific_color_maps/__init__.py +0 -0
  38. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot/scientific_color_maps/_sync_from_zenodo_download.sh +0 -0
  39. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot/scientific_color_maps/batlowK.txt +0 -0
  40. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot/scientific_color_maps/batlowW.txt +0 -0
  41. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot/scientific_color_maps/cyclic/bamO.txt +0 -0
  42. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot/scientific_color_maps/cyclic/brocO.txt +0 -0
  43. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot/scientific_color_maps/cyclic/corkO.txt +0 -0
  44. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot/scientific_color_maps/cyclic/romaO.txt +0 -0
  45. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot/scientific_color_maps/cyclic/vikO.txt +0 -0
  46. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot/scientific_color_maps/diverging/bam.txt +0 -0
  47. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot/scientific_color_maps/diverging/berlin.txt +0 -0
  48. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot/scientific_color_maps/diverging/broc.txt +0 -0
  49. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot/scientific_color_maps/diverging/cork.txt +0 -0
  50. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot/scientific_color_maps/diverging/lisbon.txt +0 -0
  51. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot/scientific_color_maps/diverging/managua.txt +0 -0
  52. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot/scientific_color_maps/diverging/roma.txt +0 -0
  53. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot/scientific_color_maps/diverging/tofino.txt +0 -0
  54. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot/scientific_color_maps/diverging/vanimo.txt +0 -0
  55. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot/scientific_color_maps/diverging/vik.txt +0 -0
  56. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot/scientific_color_maps/multisequential/bukavu.txt +0 -0
  57. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot/scientific_color_maps/multisequential/fes.txt +0 -0
  58. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot/scientific_color_maps/multisequential/oleron.txt +0 -0
  59. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot/scientific_color_maps/naviaW.txt +0 -0
  60. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot/scientific_color_maps/sequential/acton.txt +0 -0
  61. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot/scientific_color_maps/sequential/bamako.txt +0 -0
  62. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot/scientific_color_maps/sequential/batlow.txt +0 -0
  63. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot/scientific_color_maps/sequential/bilbao.txt +0 -0
  64. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot/scientific_color_maps/sequential/buda.txt +0 -0
  65. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot/scientific_color_maps/sequential/davos.txt +0 -0
  66. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot/scientific_color_maps/sequential/devon.txt +0 -0
  67. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot/scientific_color_maps/sequential/glasgow.txt +0 -0
  68. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot/scientific_color_maps/sequential/grayC.txt +0 -0
  69. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot/scientific_color_maps/sequential/hawaii.txt +0 -0
  70. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot/scientific_color_maps/sequential/imola.txt +0 -0
  71. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot/scientific_color_maps/sequential/lajolla.txt +0 -0
  72. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot/scientific_color_maps/sequential/lapaz.txt +0 -0
  73. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot/scientific_color_maps/sequential/lipari.txt +0 -0
  74. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot/scientific_color_maps/sequential/navia.txt +0 -0
  75. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot/scientific_color_maps/sequential/nuuk.txt +0 -0
  76. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot/scientific_color_maps/sequential/oslo.txt +0 -0
  77. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot/scientific_color_maps/sequential/tokyo.txt +0 -0
  78. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot/scientific_color_maps/sequential/turku.txt +0 -0
  79. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot/styles/bss.mplstyle +0 -0
  80. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot/styles/bwcomp.mplstyle +0 -0
  81. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot/styles/nature.mplstyle +0 -0
  82. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot/styles/transparent.mplstyle +0 -0
  83. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot/styles/tvbo.mplstyle +0 -0
  84. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot/text2obj.py +0 -0
  85. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot/timeseries.py +0 -0
  86. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot/utils.py +0 -0
  87. {bsplot-0.0.8 → bsplot-0.0.9}/bsplot/volume.py +0 -0
  88. {bsplot-0.0.8 → bsplot-0.0.9}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bsplot
3
- Version: 0.0.8
3
+ Version: 0.0.9
4
4
  Summary: Plotting utilities for neuroscience: styles, palettes, surfaces, glass-brain, and panel helpers.
5
5
  License-Expression: EUPL-1.2
6
6
  Project-URL: Repository, https://github.com/leon-k-martin/bsplot
@@ -21,6 +21,7 @@ Requires-Dist: quarto; extra == "docs"
21
21
  Requires-Dist: quartodoc; extra == "docs"
22
22
  Requires-Dist: griffe<1.0; extra == "docs"
23
23
  Requires-Dist: jupyter-cache; extra == "docs"
24
+ Requires-Dist: cairosvg; extra == "docs"
24
25
  Dynamic: license-file
25
26
 
26
27
  <h1>
@@ -1,7 +1,7 @@
1
1
  # Copyright © Charité Universitätsmedizin Berlin. This software is licensed under the terms of the European Union Public Licence (EUPL) version 1.2 or later.
2
2
  from os.path import abspath, dirname, join
3
3
 
4
- __version__ = "0.0.8"
4
+ __version__ = "0.0.9"
5
5
 
6
6
  ROOT = abspath(dirname(__file__))
7
7
  datadir = join(ROOT, "data")
Binary file
Binary file
@@ -1,12 +1,11 @@
1
1
  # Copyright © Charité Universitätsmedizin Berlin. This software is licensed under the terms of the European Union Public Licence (EUPL) version 1.2 or later.
2
2
  import numpy as np
3
- from matplotlib.colors import Normalize
4
- from bsplot.colors import Colormap as _resolve_cmap
5
-
6
- import numpy as np
3
+ from matplotlib import colormaps
7
4
  from matplotlib.colors import Normalize
8
5
  from matplotlib.patches import FancyArrowPatch
9
6
 
7
+ from bsplot.colors import Colormap as _resolve_cmap
8
+
10
9
 
11
10
  def get_edge_color_mapping(G, colormap="viridis", color_by="type"):
12
11
  """
@@ -197,13 +196,8 @@ def draw_custom_edges(
197
196
  # if "mutation_scale" not in kwargs.keys():
198
197
  # kwargs["mutation_scale"] = 10.0
199
198
 
200
- if edge_colors in colormaps.keys():
201
- cmap = (
202
- edge_colors
203
- if isinstance(edge_colors, str) and edge_colors in colormaps.keys()
204
- else "viridis"
205
- )
206
- colmap = get_edge_color_mapping(G, colormap=cmap, color_by=color_by)
199
+ if isinstance(edge_colors, str) and edge_colors in colormaps:
200
+ colmap = get_edge_color_mapping(G, colormap=edge_colors, color_by=color_by)
207
201
  edge_colors = [
208
202
  colmap[data[color_by] if color_by in data else "n.a."]
209
203
  for (u, v, data) in G.edges(data=True)
@@ -155,6 +155,9 @@ def plot_tractogram(
155
155
  if datakey not in data:
156
156
  raise ValueError(f"Data key '{datakey}' not found in streamlines data.")
157
157
  values = data[datakey]
158
+ arr = np.asarray(values)
159
+ if arr.ndim == 2 and arr.shape[1] == 1:
160
+ values = arr.ravel()
158
161
 
159
162
  def _is_pointwise_values(values_in, streamlines_in):
160
163
  if values_in is None:
@@ -9,45 +9,190 @@ import bsplot # Import your package to access its path
9
9
  from .colors import make_cycler_from_cmap
10
10
 
11
11
 
12
+ def _styles_dir():
13
+ """Directory holding the built-in ``.mplstyle`` modules shipped with bsplot."""
14
+ return os.path.join(os.path.dirname(bsplot.__file__), "styles")
15
+
16
+
17
+ def _available_styles():
18
+ """Sorted names of the built-in style modules (without the extension)."""
19
+ return sorted(
20
+ f.removesuffix(".mplstyle")
21
+ for f in os.listdir(_styles_dir())
22
+ if f.endswith(".mplstyle")
23
+ )
24
+
25
+
26
+ def _named_style_path(name):
27
+ """Resolve a built-in style module *name* to its ``.mplstyle`` path."""
28
+ p = os.path.join(_styles_dir(), f"{name}.mplstyle")
29
+ if not os.path.exists(p):
30
+ raise ValueError(
31
+ f"Style module '{name}' not found. "
32
+ f"Available modules: {_available_styles()}. "
33
+ f"To load a custom style file, pass a path ending in '.mplstyle'."
34
+ )
35
+ return p
36
+
37
+
38
+ def _looks_like_path(token):
39
+ """True if *token* should be treated as a filesystem path, not a module name."""
40
+ # os.PathLike (e.g. pathlib.Path) is always a filesystem path.
41
+ if isinstance(token, os.PathLike):
42
+ return True
43
+ return (
44
+ os.sep in token
45
+ or (os.altsep is not None and os.altsep in token)
46
+ or token.endswith(".mplstyle")
47
+ or os.path.isfile(token)
48
+ )
49
+
50
+
51
+ def _resolve_style_token(token):
52
+ """Resolve one token to a list of ``.mplstyle`` paths.
53
+
54
+ A token that looks like a path is loaded as a file (never split on ``-``);
55
+ otherwise it is a built-in module name, optionally composed with ``-``.
56
+ """
57
+ if _looks_like_path(token):
58
+ path = os.path.expanduser(os.fspath(token))
59
+ if not os.path.isfile(path):
60
+ raise ValueError(f"Style file '{token}' not found.")
61
+ return [path]
62
+ return [_named_style_path(m) for m in token.split("-")]
63
+
64
+
12
65
  def use(style="bss"):
13
- """Apply one or more bsplot style modules.
66
+ """Apply one or more bsplot styles.
67
+
68
+ Styles are layered left-to-right, so later styles override earlier ones.
69
+ Three kinds of style token are accepted and can be freely mixed:
70
+
71
+ * **Built-in modules** — the ``.mplstyle`` files shipped with bsplot,
72
+ referenced by name (e.g. ``"black"``, ``"tvbo"``, ``"transparent"``).
73
+ * **Composed modules** — several built-in modules joined with ``-``
74
+ in a single string (e.g. ``"black-transparent"``).
75
+ * **Filesystem paths** — a path to any ``.mplstyle`` file on disk, so a
76
+ custom style can be shipped alongside a study and combined with a base.
14
77
 
15
- Styles can be composed by separating modules with ``-``.
16
- Modules are layered left-to-right, so later modules override earlier ones.
78
+ Custom style files should stay minimal (only the rcParams they override)
79
+ so they layer cleanly on top of a base module.
80
+
81
+ Parameters
82
+ ----------
83
+ style : str, os.PathLike, or list of these
84
+ A single token, a ``-``-composed string of built-in modules, or a
85
+ list mixing module names and filesystem paths. Anything that looks
86
+ like a path (contains a path separator, ends in ``.mplstyle``, is a
87
+ :class:`~os.PathLike`, or names an existing file) is loaded as a file
88
+ and is never split on ``-``.
17
89
 
18
90
  Examples
19
91
  --------
20
- >>> bsplot.style.use("black") # single base style
21
- >>> bsplot.style.use("black-transparent") # base + module
22
- >>> bsplot.style.use("tvbo-transparent") # works with any base
92
+ >>> bsplot.style.use("black") # single base module
93
+ >>> bsplot.style.use("black-transparent") # base + module
94
+ >>> bsplot.style.use("tvbo-transparent") # works with any base
95
+ >>> bsplot.style.use("study/custom.mplstyle") # a custom style file
96
+ >>> bsplot.style.use(["black", "study/custom.mplstyle"]) # base + custom
97
+ >>> bsplot.style.use(["tvbo", "study/custom.mplstyle"]) # combine freely
23
98
  """
24
- styles_dir = os.path.join(os.path.dirname(bsplot.__file__), "styles")
25
-
26
- def _style_path(name):
27
- p = os.path.join(styles_dir, f"{name}.mplstyle")
28
- if not os.path.exists(p):
29
- available = sorted(
30
- f.removesuffix(".mplstyle")
31
- for f in os.listdir(styles_dir)
32
- if f.endswith(".mplstyle")
33
- )
34
- raise ValueError(
35
- f"Style module '{name}' not found. "
36
- f"Available modules: {available}"
37
- )
38
- return p
39
-
40
- # Split on "-" to get composable modules: "black-transparent" -> ["black", "transparent"]
41
- modules = style.split("-")
42
- paths = [_style_path(m) for m in modules]
99
+ # Normalise to a flat, ordered list of style-file paths.
100
+ tokens = style if isinstance(style, (list, tuple)) else [style]
101
+ paths = []
102
+ for tok in tokens:
103
+ paths.extend(_resolve_style_token(tok))
43
104
  plt.style.use(paths)
44
105
 
45
106
  # Turn off legacy layout flags that can conflict
46
107
  plt.rcParams["figure.constrained_layout.use"] = False
47
108
  plt.rcParams["figure.autolayout"] = False
48
109
 
49
- if modules[0] == "black":
50
- plt.rcParams["axes.prop_cycle"]._left[0] = {"color": "white"}
110
+
111
+ def create_template(path=None, base=None, overwrite=False):
112
+ """Write a ready-to-edit ``.mplstyle`` style template to disk.
113
+
114
+ Scaffolds a fully functional style file you can customise and ship
115
+ alongside a study or project. The result can be applied on its own or
116
+ combined with a bsplot base style via :func:`use` (e.g.
117
+ ``bsplot.style.use(["black", "my_study.mplstyle"])``).
118
+
119
+ Parameters
120
+ ----------
121
+ path : str, os.PathLike, or None
122
+ Where to write the template. If ``None`` (default), writes
123
+ ``bsplot_custom.mplstyle`` in the current directory. A ``.mplstyle``
124
+ extension is appended when missing, and a directory target writes
125
+ ``bsplot_custom.mplstyle`` inside it.
126
+ base : str, os.PathLike, or None
127
+ Seed the template from an existing style. May be the name of a
128
+ built-in module (e.g. ``"black"``, ``"tvbo"`` — see
129
+ :func:`_available_styles`) or a path to any ``.mplstyle`` file. When
130
+ ``None`` (default), a complete curated starter template is written.
131
+ overwrite : bool
132
+ If ``False`` (default), refuse to clobber an existing file.
133
+
134
+ Returns
135
+ -------
136
+ str
137
+ The path that was written.
138
+
139
+ Examples
140
+ --------
141
+ >>> bsplot.style.create_template() # bsplot_custom.mplstyle
142
+ >>> bsplot.style.create_template("study/theme.mplstyle")
143
+ >>> bsplot.style.create_template("study/dark.mplstyle", base="black")
144
+ >>> bsplot.style.use(["black", "study/dark.mplstyle"]) # combine base + custom
145
+ """
146
+ if path is None:
147
+ path = "bsplot_custom.mplstyle"
148
+ path = os.path.expanduser(os.fspath(path))
149
+ if os.path.isdir(path):
150
+ path = os.path.join(path, "bsplot_custom.mplstyle")
151
+ if not path.endswith(".mplstyle"):
152
+ path += ".mplstyle"
153
+
154
+ if os.path.exists(path) and not overwrite:
155
+ raise FileExistsError(
156
+ f"'{path}' already exists. Pass overwrite=True to replace it, "
157
+ f"or choose a different path."
158
+ )
159
+
160
+ if base is None:
161
+ # The shipped, self-documenting default style — also available as
162
+ # ``bsplot.style.use("template")``. Copied verbatim; its own header
163
+ # explains how to edit and combine it.
164
+ with open(_named_style_path("template"), encoding="utf-8") as fh:
165
+ content = fh.read()
166
+ else:
167
+ bodies = []
168
+ for src in _resolve_style_token(base):
169
+ with open(src, "r", encoding="utf-8") as fh:
170
+ bodies.append(fh.read().strip())
171
+ body = "\n\n".join(bodies)
172
+ # Show the exact base string in the combine hint when it is a module
173
+ # name (or "-" composition); fall back to a placeholder for file bases.
174
+ combine_base = base if not _looks_like_path(base) else "<base-style>"
175
+ fname = os.path.basename(path)
176
+ header = f"""\
177
+ # Custom bsplot style — seeded from '{base}'.
178
+ #
179
+ # Edit the values below, then apply it directly:
180
+ # bsplot.style.use("{fname}")
181
+ #
182
+ # It also layers on top of any base style (later wins), so you can
183
+ # trim this down to only the keys you override and combine:
184
+ # bsplot.style.use(["{combine_base}", "{fname}"])
185
+ #
186
+ # rcParams reference:
187
+ # https://matplotlib.org/stable/users/explain/customizing.html
188
+ """
189
+ content = header + "\n" + body + "\n"
190
+
191
+ parent = os.path.dirname(os.path.abspath(path))
192
+ os.makedirs(parent, exist_ok=True)
193
+ with open(path, "w", encoding="utf-8") as f:
194
+ f.write(content)
195
+ return path
51
196
 
52
197
 
53
198
  def format(obj, **kwargs):
@@ -125,7 +270,12 @@ def format(obj, **kwargs):
125
270
 
126
271
 
127
272
  def format_fig(
128
- fig, colorbar_decimals=None, tick_decimals=None, add_panel_numbers="auto", **kwargs
273
+ fig,
274
+ colorbar_decimals=None,
275
+ tick_decimals=None,
276
+ add_panel_numbers="auto",
277
+ align_labels=True,
278
+ **kwargs,
129
279
  ):
130
280
  """Format all axes in a figure.
131
281
 
@@ -134,8 +284,11 @@ def format_fig(
134
284
  If None or "smart", uses automatic detection based on data range.
135
285
  - colorbar_decimals: int | None -> number of decimals for colorbar ticks.
136
286
  If None, uses automatic detection based on data range.
287
+ - align_labels: bool -> if True, align x- and y-axis labels across subplots
288
+ via ``fig.align_labels()``.
137
289
  - other kwargs are passed to format_ax/format_colorbar as appropriate
138
290
  """
291
+ fig.set_layout_engine("compressed")
139
292
  i = 1
140
293
  for ax in fig.axes:
141
294
  # Skip polar (and other non-rectilinear) axes – they lack standard spines
@@ -164,6 +317,9 @@ def format_fig(
164
317
  )
165
318
  i += 1
166
319
 
320
+ if align_labels:
321
+ fig.align_labels()
322
+
167
323
 
168
324
  def format_all(**kwargs):
169
325
  """Format every open matplotlib figure.
@@ -44,7 +44,6 @@ grid.color: 0.4
44
44
  # --- Figure ---
45
45
  figure.dpi: 150
46
46
  figure.figsize: 7.2, 5.0
47
- figure.constrained_layout.use: True
48
47
  figure.facecolor: black
49
48
  figure.edgecolor: black
50
49
 
@@ -0,0 +1,83 @@
1
+ # ============================================================================
2
+ # bsplot custom style template
3
+ # ============================================================================
4
+ # A complete, ready-to-edit matplotlib style. Change any value below.
5
+ #
6
+ # Apply it directly (standalone), e.g. shipped alongside a study:
7
+ # import bsplot
8
+ # bsplot.style.use("path/to/this.mplstyle")
9
+ #
10
+ # Or layer it on top of a bsplot base style so you only override what you need.
11
+ # Later styles win, so keep just the lines you change and delete the rest:
12
+ # bsplot.style.use(["tvbo", "path/to/this.mplstyle"])
13
+ # bsplot.style.use(["black", "path/to/this.mplstyle"])
14
+ #
15
+ # Full list of options:
16
+ # https://matplotlib.org/stable/users/explain/customizing.html
17
+ # ============================================================================
18
+
19
+ # --- Axes ---
20
+ axes.facecolor: white
21
+ axes.edgecolor: black
22
+ axes.linewidth: 1.5
23
+ axes.spines.top: False
24
+ axes.spines.right: False
25
+ axes.grid: False
26
+ axes.titlesize: 10
27
+ axes.titlepad: 6
28
+ axes.titlecolor: black
29
+ axes.labelsize: 9
30
+ axes.labelpad: 4
31
+ axes.labelcolor: black
32
+
33
+ # --- Font ---
34
+ font.family: sans-serif
35
+ font.sans-serif: Helvetica, Arial, DejaVu Sans
36
+ font.size: 9
37
+ text.color: black
38
+
39
+ # --- Ticks ---
40
+ xtick.direction: out
41
+ ytick.direction: out
42
+ xtick.major.size: 6
43
+ ytick.major.size: 6
44
+ xtick.major.width: 1.5
45
+ ytick.major.width: 1.5
46
+ xtick.labelsize: 8
47
+ ytick.labelsize: 8
48
+ xtick.color: black
49
+ ytick.color: black
50
+ xtick.minor.visible: False
51
+ ytick.minor.visible: False
52
+
53
+ # --- Grid (only shown when axes.grid: True) ---
54
+ grid.color: 0.6
55
+ grid.linestyle: -
56
+ grid.linewidth: 0.5
57
+ grid.alpha: 0.3
58
+
59
+ # --- Lines and markers ---
60
+ lines.linewidth: 1.5
61
+ lines.markersize: 4
62
+ lines.antialiased: True
63
+
64
+ # --- Legend ---
65
+ legend.frameon: True
66
+ legend.fontsize: 8
67
+ legend.loc: best
68
+
69
+ # --- Figure ---
70
+ figure.figsize: 7.2, 5.0
71
+ figure.dpi: 150
72
+ figure.facecolor: white
73
+ figure.edgecolor: white
74
+
75
+ # --- Savefig (export) ---
76
+ savefig.dpi: 300
77
+ savefig.format: pdf
78
+ savefig.bbox: tight
79
+ savefig.pad_inches: 0.05
80
+ savefig.transparent: False
81
+
82
+ # --- Color cycle (colourblind-safe; edit to match your palette) ---
83
+ axes.prop_cycle: cycler(color=["#000000", "#E69F00", "#56B4E9", "#009E73", "#F0E442", "#0072B2", "#D55E00", "#CC79A7"])
@@ -0,0 +1,77 @@
1
+ # --- Axes ---
2
+ axes.linewidth: 1.5
3
+ axes.edgecolor: black
4
+ axes.spines.top: False
5
+ axes.spines.right: False
6
+ axes.autolimit_mode: data
7
+ axes.titlesize: 10
8
+ axes.labelsize: 9
9
+ axes.labelpad: 4
10
+ axes.titlepad: 6
11
+ axes.facecolor: white
12
+
13
+ # --- Font ---
14
+ font.size: 9
15
+ font.family: sans-serif
16
+ font.sans-serif: Helvetica, Arial, DejaVu Sans
17
+ text.color: black
18
+ axes.labelcolor: black
19
+ axes.titlecolor: black
20
+
21
+ # --- Ticks ---
22
+ xtick.direction: out
23
+ ytick.direction: out
24
+ xtick.major.size: 6
25
+ ytick.major.size: 6
26
+ xtick.major.width: 1.5
27
+ ytick.major.width: 1.5
28
+ xtick.minor.visible: False
29
+ ytick.minor.visible: False
30
+ xtick.bottom: True
31
+ ytick.left: True
32
+ xtick.labelsize: 8
33
+ ytick.labelsize: 8
34
+ xtick.color: black
35
+ ytick.color: black
36
+
37
+ # --- Grid ---
38
+ axes.grid: False
39
+ grid.linestyle: -
40
+ grid.linewidth: 0.5
41
+ grid.alpha: 0.3
42
+ grid.color: 0.6
43
+
44
+ # --- Figure ---
45
+ figure.dpi: 150
46
+ figure.figsize: 7.2, 5.0
47
+ figure.facecolor: white
48
+ figure.edgecolor: white
49
+
50
+ # --- Savefig ---
51
+ savefig.dpi: 300
52
+ savefig.bbox: tight
53
+ savefig.pad_inches: 0.05
54
+ savefig.format: pdf
55
+ savefig.facecolor: white
56
+ savefig.edgecolor: white
57
+ savefig.transparent: False
58
+
59
+ # --- Lines and markers ---
60
+ lines.linewidth: 1.5
61
+ lines.markersize: 4
62
+
63
+ # --- Legend ---
64
+ legend.frameon: True
65
+ legend.fontsize: 8
66
+ legend.loc: best
67
+ legend.facecolor: 0.97
68
+ legend.edgecolor: black
69
+ legend.framealpha: 0.8
70
+ legend.labelcolor: black
71
+
72
+ # --- Patches ---
73
+ patch.edgecolor: black
74
+ patch.force_edgecolor: True
75
+
76
+ # --- Color cycle ---
77
+ axes.prop_cycle: cycler(color=["#000000", "#E69F00", "#56B4E9", "#009E73", "#F0E442", "#0072B2", "#D55E00", "#CC79A7"])
@@ -1,4 +1,5 @@
1
1
  # Copyright © Charité Universitätsmedizin Berlin. This software is licensed under the terms of the European Union Public Licence (EUPL) version 1.2 or later.
2
+ from __future__ import annotations
2
3
  import os
3
4
  import matplotlib.pyplot as plt
4
5
  import nibabel
@@ -1043,6 +1044,8 @@ def plot_surf(
1043
1044
  view="lateral",
1044
1045
  cmap="viridis",
1045
1046
  color=None, # Base surface color when no overlay
1047
+ shade_mode="auto", # "darken" (multiply), "lighten" (add white), or "auto"
1048
+ shade_intensity=1.0, # strength of the geometric shading (0 = none, 1 = full)
1046
1049
  label=False,
1047
1050
  vmax=None,
1048
1051
  vmin=None,
@@ -1130,6 +1133,21 @@ def plot_surf(
1130
1133
  (or *threshold_color*). When ``overlay_volume`` is used and
1131
1134
  *sampling_threshold* is not set, *threshold* is also used as the
1132
1135
  sampling threshold.
1136
+ shade_mode : {"auto", "darken", "lighten"}, default "auto"
1137
+ How geometric shading is applied to the base colour.
1138
+ ``"darken"`` multiplies toward black (the classic behaviour; lit
1139
+ faces keep the base colour, shadowed faces darken). This shows no
1140
+ structure on near-black surfaces. ``"lighten"`` instead adds white
1141
+ toward the *lit* faces, so structure is visible on dark colours
1142
+ (e.g. ``color="k"``) while the apparent light source stays on the
1143
+ same side as ``"darken"``. ``"auto"`` picks ``"lighten"`` only when
1144
+ the base colour is very dark and ``"darken"`` otherwise.
1145
+ shade_intensity : float, default 1.0
1146
+ Strength of the geometric shading. ``0`` disables shading (flat base
1147
+ colour); ``1`` is full strength. In ``"lighten"`` mode it scales how
1148
+ much white is added to lit faces (lower = darker, more of the base
1149
+ colour shows through); in ``"darken"`` mode it scales how much
1150
+ shadowed faces are darkened.
1133
1151
  """
1134
1152
  # Handle alpha as alias for transparency
1135
1153
  if alpha is not None:
@@ -1867,8 +1885,35 @@ def plot_surf(
1867
1885
  # Capture pre-shading face color for legend proxy (before shading darkens it)
1868
1886
  _legend_color = C[0].copy() if (label and label is not False) else None
1869
1887
 
1870
- # Apply lighting intensity to RGB channels
1871
- C[:, 0:3] *= intensity[:, None]
1888
+ # Apply lighting intensity to RGB channels.
1889
+ #
1890
+ # intensity runs from (1 - shading) ≈ 0.3 on faces pointing away from
1891
+ # the light up to 1.0 on faces facing it. Two ways to render that:
1892
+ # "darken" – multiply toward black; shadows darken the base colour.
1893
+ # Looks right for bright colours but does nothing on
1894
+ # black (0 * anything == 0).
1895
+ # "lighten" – add white toward the *lit* faces (high intensity), so
1896
+ # structure shows up on dark surfaces while the light
1897
+ # source stays on the same side as the darken mode.
1898
+ _mode = shade_mode
1899
+ if _mode == "auto":
1900
+ # Lighten when the base colour is too dark for multiply to show
1901
+ # structure; otherwise keep the classic darken behaviour.
1902
+ _mode = "lighten" if float(np.max(C[:, 0:3])) < 0.25 else "darken"
1903
+ if _mode == "lighten":
1904
+ _t_min = float(np.min(intensity))
1905
+ _span = 1.0 - _t_min
1906
+ if _span == 0:
1907
+ _span = 1.0
1908
+ # hl: 0 in deepest shadow, 1 on fully-lit faces (same direction as
1909
+ # the multiply, just applied as a highlight rather than a shadow).
1910
+ # shade_intensity scales how much white is added (1 = full white).
1911
+ hl = ((intensity - _t_min) / _span)[:, None] * shade_intensity
1912
+ C[:, 0:3] = np.clip(C[:, 0:3] + (1.0 - C[:, 0:3]) * hl, 0.0, 1.0)
1913
+ else:
1914
+ # shade_intensity scales how much shadows darken (1 = full).
1915
+ eff = 1.0 - (1.0 - intensity) * shade_intensity
1916
+ C[:, 0:3] *= eff[:, None]
1872
1917
 
1873
1918
  # Perspective MVP for robust drawing
1874
1919
  MVP = (
@@ -2096,8 +2141,26 @@ def plot_surf(
2096
2141
  is_below = np.any(add_overlay[add_F] <= threshold, axis=1)
2097
2142
  add_C[is_below] = grey
2098
2143
 
2099
- # Apply shading intensity
2100
- add_C[:, 0:3] *= add_intensity[:, None]
2144
+ # Apply shading intensity (same darken/lighten logic as main)
2145
+ _add_mode = shade_mode
2146
+ if _add_mode == "auto":
2147
+ _add_mode = (
2148
+ "lighten"
2149
+ if float(np.max(add_C[:, 0:3])) < 0.25
2150
+ else "darken"
2151
+ )
2152
+ if _add_mode == "lighten":
2153
+ _t_min = float(np.min(add_intensity))
2154
+ _span = 1.0 - _t_min
2155
+ if _span == 0:
2156
+ _span = 1.0
2157
+ hl = ((add_intensity - _t_min) / _span)[:, None] * shade_intensity
2158
+ add_C[:, 0:3] = np.clip(
2159
+ add_C[:, 0:3] + (1.0 - add_C[:, 0:3]) * hl, 0.0, 1.0
2160
+ )
2161
+ else:
2162
+ eff = 1.0 - (1.0 - add_intensity) * shade_intensity
2163
+ add_C[:, 0:3] *= eff[:, None]
2101
2164
 
2102
2165
  # Project vertices
2103
2166
  add_V_all = np.c_[add_proj_verts, np.ones(len(add_proj_verts))] @ MVP.T
@@ -1,5 +1,6 @@
1
1
  # Copyright © Charité Universitätsmedizin Berlin. This software is licensed under the terms of the European Union Public Licence (EUPL) version 1.2 or later.
2
2
  """Neuroimaging template volumes, downloaded on first access to ~/.bsplot/templates/."""
3
+ from __future__ import annotations
3
4
 
4
5
  import urllib.request
5
6
  from pathlib import Path
@@ -1,4 +1,5 @@
1
1
  # Copyright © Charité Universitätsmedizin Berlin. This software is licensed under the terms of the European Union Public Licence (EUPL) version 1.2 or later.
2
+ from __future__ import annotations
2
3
  import black
3
4
  import matplotlib.pyplot as plt
4
5
  from pygments.lexers import PythonLexer, get_lexer_by_name
@@ -21,6 +21,8 @@ bsplot/utils.py
21
21
  bsplot/volume.py
22
22
  bsplot/data/HCP_avg-SC.txt
23
23
  bsplot/data/MNI152.rh.pial
24
+ bsplot/data/STN_lh.nii.gz
25
+ bsplot/data/STN_rh.nii.gz
24
26
  bsplot/data/__init__.py
25
27
  bsplot/data/surface.py
26
28
  bsplot/data/parcellations/HCP-MMP1.L.label.gii
@@ -77,5 +79,7 @@ bsplot/styles/black.mplstyle
77
79
  bsplot/styles/bss.mplstyle
78
80
  bsplot/styles/bwcomp.mplstyle
79
81
  bsplot/styles/nature.mplstyle
82
+ bsplot/styles/template.mplstyle
80
83
  bsplot/styles/transparent.mplstyle
81
- bsplot/styles/tvbo.mplstyle
84
+ bsplot/styles/tvbo.mplstyle
85
+ bsplot/styles/white.mplstyle
@@ -23,7 +23,7 @@ dependencies = [
23
23
  ]
24
24
 
25
25
  [project.optional-dependencies]
26
- docs = ["quarto", "quartodoc", "griffe<1.0", "jupyter-cache"]
26
+ docs = ["quarto", "quartodoc", "griffe<1.0", "jupyter-cache", "cairosvg"]
27
27
 
28
28
  [project.urls]
29
29
  Repository = "https://github.com/leon-k-martin/bsplot"
@@ -42,6 +42,8 @@ include = ["bsplot*"]
42
42
  "styles/*.mplstyle",
43
43
  "data/HCP_avg-SC.txt",
44
44
  "data/MNI152.rh.pial",
45
+ "data/STN_lh.nii.gz",
46
+ "data/STN_rh.nii.gz",
45
47
  "data/parcellations/HCP-MMP1.L.label.gii",
46
48
  "data/parcellations/HCP-MMP1.R.label.gii",
47
49
  "scientific_color_maps/*",
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes