bsplot 0.0.2__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.
- bsplot/__init__.py +23 -0
- bsplot/anat.py +55 -0
- bsplot/animate.py +194 -0
- bsplot/bioicons.py +777 -0
- bsplot/brain.py +188 -0
- bsplot/colors.py +1935 -0
- bsplot/data/HCP_avg-SC.txt +379 -0
- bsplot/data/MNI152.rh.pial +0 -0
- bsplot/data/STN_lh.nii.gz +0 -0
- bsplot/data/STN_rh.nii.gz +0 -0
- bsplot/data/__init__.py +22 -0
- bsplot/data/left_electrode.ply +257270 -0
- bsplot/data/parcellations/HCP-MMP1.L.label.gii +442 -0
- bsplot/data/parcellations/HCP-MMP1.R.label.gii +442 -0
- bsplot/data/parcellations/code/split_hcpmmp1.sh +4 -0
- bsplot/data/right_electrode.ply +257270 -0
- bsplot/data/surface.py +112 -0
- bsplot/data/tpl-MNI152NLin2009b_atlas-hcpmmp1_desc-ordered_dseg.nii.gz +0 -0
- bsplot/figure.py +177 -0
- bsplot/graph/__init__.py +31 -0
- bsplot/graph/edges.py +312 -0
- bsplot/graph/flowchart.py +762 -0
- bsplot/graph/layout.py +201 -0
- bsplot/graph/network.py +974 -0
- bsplot/graph/nodes.py +452 -0
- bsplot/panels.py +110 -0
- bsplot/scientific_color_maps/__init__.py +0 -0
- bsplot/scientific_color_maps/_sync_from_zenodo_download.sh +1 -0
- bsplot/scientific_color_maps/batlowK.txt +256 -0
- bsplot/scientific_color_maps/batlowW.txt +256 -0
- bsplot/scientific_color_maps/cyclic/bamO.txt +256 -0
- bsplot/scientific_color_maps/cyclic/brocO.txt +256 -0
- bsplot/scientific_color_maps/cyclic/corkO.txt +256 -0
- bsplot/scientific_color_maps/cyclic/romaO.txt +256 -0
- bsplot/scientific_color_maps/cyclic/vikO.txt +256 -0
- bsplot/scientific_color_maps/diverging/bam.txt +256 -0
- bsplot/scientific_color_maps/diverging/berlin.txt +256 -0
- bsplot/scientific_color_maps/diverging/broc.txt +256 -0
- bsplot/scientific_color_maps/diverging/cork.txt +256 -0
- bsplot/scientific_color_maps/diverging/lisbon.txt +256 -0
- bsplot/scientific_color_maps/diverging/managua.txt +256 -0
- bsplot/scientific_color_maps/diverging/roma.txt +256 -0
- bsplot/scientific_color_maps/diverging/tofino.txt +256 -0
- bsplot/scientific_color_maps/diverging/vanimo.txt +256 -0
- bsplot/scientific_color_maps/diverging/vik.txt +256 -0
- bsplot/scientific_color_maps/multisequential/bukavu.txt +256 -0
- bsplot/scientific_color_maps/multisequential/fes.txt +256 -0
- bsplot/scientific_color_maps/multisequential/oleron.txt +256 -0
- bsplot/scientific_color_maps/naviaW.txt +256 -0
- bsplot/scientific_color_maps/sequential/acton.txt +256 -0
- bsplot/scientific_color_maps/sequential/bamako.txt +256 -0
- bsplot/scientific_color_maps/sequential/batlow.txt +256 -0
- bsplot/scientific_color_maps/sequential/bilbao.txt +256 -0
- bsplot/scientific_color_maps/sequential/buda.txt +256 -0
- bsplot/scientific_color_maps/sequential/davos.txt +256 -0
- bsplot/scientific_color_maps/sequential/devon.txt +256 -0
- bsplot/scientific_color_maps/sequential/glasgow.txt +256 -0
- bsplot/scientific_color_maps/sequential/grayC.txt +256 -0
- bsplot/scientific_color_maps/sequential/hawaii.txt +256 -0
- bsplot/scientific_color_maps/sequential/imola.txt +256 -0
- bsplot/scientific_color_maps/sequential/lajolla.txt +256 -0
- bsplot/scientific_color_maps/sequential/lapaz.txt +256 -0
- bsplot/scientific_color_maps/sequential/lipari.txt +256 -0
- bsplot/scientific_color_maps/sequential/navia.txt +256 -0
- bsplot/scientific_color_maps/sequential/nuuk.txt +256 -0
- bsplot/scientific_color_maps/sequential/oslo.txt +256 -0
- bsplot/scientific_color_maps/sequential/tokyo.txt +256 -0
- bsplot/scientific_color_maps/sequential/turku.txt +256 -0
- bsplot/streamlines.py +314 -0
- bsplot/style.py +791 -0
- bsplot/styles/black.mplstyle +78 -0
- bsplot/styles/bss.mplstyle +50 -0
- bsplot/styles/bwcomp.mplstyle +30 -0
- bsplot/styles/nature.mplstyle +57 -0
- bsplot/styles/transparent.mplstyle +16 -0
- bsplot/styles/tvbo.mplstyle +57 -0
- bsplot/surface.py +2370 -0
- bsplot/templates.py +270 -0
- bsplot/text.py +199 -0
- bsplot/text2obj.py +59 -0
- bsplot/timeseries.py +10 -0
- bsplot/utils.py +13 -0
- bsplot/volume.py +958 -0
- bsplot-0.0.2.dist-info/METADATA +60 -0
- bsplot-0.0.2.dist-info/RECORD +89 -0
- bsplot-0.0.2.dist-info/WHEEL +5 -0
- bsplot-0.0.2.dist-info/entry_points.txt +2 -0
- bsplot-0.0.2.dist-info/licenses/LICENSE +193 -0
- bsplot-0.0.2.dist-info/top_level.txt +1 -0
bsplot/templates.py
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"""Neuroimaging template volumes, downloaded on first access to ~/.bsplot/templates/."""
|
|
2
|
+
|
|
3
|
+
import urllib.request
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import templateflow.api as tflow
|
|
7
|
+
|
|
8
|
+
from bsplot import datadir
|
|
9
|
+
|
|
10
|
+
_CACHE_DIR = Path.home() / ".bsplot" / "templates"
|
|
11
|
+
_LEGACY_DIR = Path(datadir)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# ---------------------------------------------------------------------------
|
|
15
|
+
# Core infrastructure
|
|
16
|
+
# ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _download(url: str, dest: Path, label: str = "") -> None:
|
|
20
|
+
"""Download *url* → *dest* with a progress bar."""
|
|
21
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
22
|
+
tmp = dest.with_suffix(".part")
|
|
23
|
+
print(f"Downloading {label} from\n {url}")
|
|
24
|
+
try:
|
|
25
|
+
resp = urllib.request.urlopen(url)
|
|
26
|
+
total = int(resp.headers.get("Content-Length", 0))
|
|
27
|
+
done = 0
|
|
28
|
+
with open(tmp, "wb") as f:
|
|
29
|
+
while chunk := resp.read(1 << 20):
|
|
30
|
+
f.write(chunk)
|
|
31
|
+
done += len(chunk)
|
|
32
|
+
if total:
|
|
33
|
+
print(f"\r {done/1e6:.1f}/{total/1e6:.1f} MB ({done/total*100:.0f}%)",
|
|
34
|
+
end="", flush=True)
|
|
35
|
+
print()
|
|
36
|
+
tmp.rename(dest)
|
|
37
|
+
except Exception:
|
|
38
|
+
tmp.unlink(missing_ok=True)
|
|
39
|
+
raise
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class Template:
|
|
43
|
+
"""A remote neuroimaging template with multiple resolutions.
|
|
44
|
+
|
|
45
|
+
Acts as an ``os.PathLike`` for the default resolution (lazy download),
|
|
46
|
+
so it can be passed directly to ``nibabel.load()`` or ``plot_slice()``.
|
|
47
|
+
|
|
48
|
+
Parameters
|
|
49
|
+
----------
|
|
50
|
+
name : str
|
|
51
|
+
Human-readable name shown in progress messages and repr.
|
|
52
|
+
files : dict[str, tuple[str, str]]
|
|
53
|
+
``{resolution: (filename, url)}`` for every available variant.
|
|
54
|
+
default : str
|
|
55
|
+
Key into *files* used when the template is accessed as a path.
|
|
56
|
+
migrate_legacy : bool
|
|
57
|
+
If True, check ``bsplot/data/`` for a bundled copy before downloading.
|
|
58
|
+
postprocess : callable or None
|
|
59
|
+
``f(raw_path) -> derived_path``. Called once after the first
|
|
60
|
+
download to create a derived file (e.g. background-masked).
|
|
61
|
+
The *derived* path is what ``get()`` returns.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
def __init__(self, name: str, files: dict, default: str, *,
|
|
65
|
+
migrate_legacy: bool = False, postprocess=None):
|
|
66
|
+
self._name = name
|
|
67
|
+
self._files = files
|
|
68
|
+
self._default = default
|
|
69
|
+
self._migrate = migrate_legacy
|
|
70
|
+
self._postprocess = postprocess
|
|
71
|
+
self._resolved: dict[str, Path] = {}
|
|
72
|
+
|
|
73
|
+
# -- public API ---------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def resolutions(self) -> list[str]:
|
|
77
|
+
"""Available resolution strings."""
|
|
78
|
+
return list(self._files)
|
|
79
|
+
|
|
80
|
+
def get(self, resolution: str | None = None) -> Path:
|
|
81
|
+
"""Return the path for *resolution*, downloading if necessary.
|
|
82
|
+
|
|
83
|
+
If a ``postprocess`` function was provided, returns the derived
|
|
84
|
+
(post-processed) path. Use :meth:`get_raw` for the original.
|
|
85
|
+
"""
|
|
86
|
+
res = resolution or self._default
|
|
87
|
+
if res in self._resolved:
|
|
88
|
+
return self._resolved[res]
|
|
89
|
+
raw = self._get_raw(res)
|
|
90
|
+
|
|
91
|
+
# Post-processing: create a derived file once, return it instead
|
|
92
|
+
if self._postprocess is not None:
|
|
93
|
+
derived = self._postprocess(raw)
|
|
94
|
+
self._resolved[res] = derived
|
|
95
|
+
return derived
|
|
96
|
+
|
|
97
|
+
self._resolved[res] = raw
|
|
98
|
+
return raw
|
|
99
|
+
|
|
100
|
+
def get_raw(self, resolution: str | None = None) -> Path:
|
|
101
|
+
"""Return the raw (unprocessed) path, bypassing any postprocess."""
|
|
102
|
+
return self._get_raw(resolution or self._default)
|
|
103
|
+
|
|
104
|
+
def _get_raw(self, res: str) -> Path:
|
|
105
|
+
if res not in self._files:
|
|
106
|
+
raise ValueError(
|
|
107
|
+
f"Unknown resolution '{res}' for {self._name}. "
|
|
108
|
+
f"Choose from {self.resolutions}."
|
|
109
|
+
)
|
|
110
|
+
fname, url = self._files[res]
|
|
111
|
+
path = _CACHE_DIR / fname
|
|
112
|
+
|
|
113
|
+
if not path.exists():
|
|
114
|
+
legacy = _LEGACY_DIR / fname
|
|
115
|
+
if self._migrate and legacy.exists():
|
|
116
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
117
|
+
legacy.rename(path)
|
|
118
|
+
else:
|
|
119
|
+
_download(url, path, label=f"{self._name} ({res})")
|
|
120
|
+
|
|
121
|
+
return path
|
|
122
|
+
|
|
123
|
+
# -- os.PathLike (default resolution, lazy) -----------------------------
|
|
124
|
+
|
|
125
|
+
def __fspath__(self) -> str:
|
|
126
|
+
return str(self.get())
|
|
127
|
+
|
|
128
|
+
def __str__(self) -> str:
|
|
129
|
+
return str(self.get())
|
|
130
|
+
|
|
131
|
+
def __repr__(self) -> str:
|
|
132
|
+
return f"Template({self._name!r}, default={self._default!r})"
|
|
133
|
+
|
|
134
|
+
def __truediv__(self, other):
|
|
135
|
+
return self.get() / other
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class TFlowTemplate:
|
|
139
|
+
"""A TemplateFlow-backed template, lazily resolved on first access.
|
|
140
|
+
|
|
141
|
+
Acts as ``os.PathLike`` so it can be passed directly to ``plot_slice()``
|
|
142
|
+
or ``nibabel.load()``.
|
|
143
|
+
|
|
144
|
+
Parameters
|
|
145
|
+
----------
|
|
146
|
+
name : str
|
|
147
|
+
Human-readable name shown in repr.
|
|
148
|
+
template : str
|
|
149
|
+
TemplateFlow template ID, e.g. ``"MNI152NLin2009cAsym"``.
|
|
150
|
+
suffix : str
|
|
151
|
+
BIDS suffix, e.g. ``"T1w"``, ``"T2w"``, ``"mask"``.
|
|
152
|
+
desc : str or None
|
|
153
|
+
BIDS desc entity, e.g. ``"brain"``.
|
|
154
|
+
resolution : int
|
|
155
|
+
Resolution index (TemplateFlow convention).
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
def __init__(self, name: str, template: str, suffix: str,
|
|
159
|
+
desc: str | None = None, resolution: int = 1):
|
|
160
|
+
self._name = name
|
|
161
|
+
self._template = template
|
|
162
|
+
self._suffix = suffix
|
|
163
|
+
self._desc = desc
|
|
164
|
+
self._resolution = resolution
|
|
165
|
+
self._path: Path | None = None
|
|
166
|
+
|
|
167
|
+
def get(self, resolution: int | None = None) -> Path:
|
|
168
|
+
"""Return the path, downloading via TemplateFlow if necessary."""
|
|
169
|
+
res = resolution or self._resolution
|
|
170
|
+
# Always re-resolve if a different resolution is requested
|
|
171
|
+
if self._path is not None and res == self._resolution:
|
|
172
|
+
return self._path
|
|
173
|
+
kwargs = dict(
|
|
174
|
+
resolution=res, suffix=self._suffix, extension=".nii.gz"
|
|
175
|
+
)
|
|
176
|
+
if self._desc is not None:
|
|
177
|
+
kwargs["desc"] = self._desc
|
|
178
|
+
result = tflow.get(self._template, **kwargs)
|
|
179
|
+
if isinstance(result, list):
|
|
180
|
+
result = result[0]
|
|
181
|
+
path = Path(result)
|
|
182
|
+
if res == self._resolution:
|
|
183
|
+
self._path = path
|
|
184
|
+
return path
|
|
185
|
+
|
|
186
|
+
def __fspath__(self) -> str:
|
|
187
|
+
return str(self.get())
|
|
188
|
+
|
|
189
|
+
def __str__(self) -> str:
|
|
190
|
+
return str(self.get())
|
|
191
|
+
|
|
192
|
+
def __repr__(self) -> str:
|
|
193
|
+
parts = [self._template, self._suffix]
|
|
194
|
+
if self._desc:
|
|
195
|
+
parts.append(f"desc-{self._desc}")
|
|
196
|
+
return f"TFlowTemplate({', '.join(parts)})"
|
|
197
|
+
|
|
198
|
+
def __truediv__(self, other):
|
|
199
|
+
return self.get() / other
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
# ---------------------------------------------------------------------------
|
|
203
|
+
# Template definitions
|
|
204
|
+
# ---------------------------------------------------------------------------
|
|
205
|
+
|
|
206
|
+
_BB = "https://ftp.bigbrainproject.org/bigbrain-ftp/BigBrainRelease.2015/3D_Volumes/MNI-ICBM152_Space/nii"
|
|
207
|
+
_DD = "https://datadryad.org/downloads/file_stream"
|
|
208
|
+
|
|
209
|
+
# ---------------------------------------------------------------------------
|
|
210
|
+
# MNI 152 templates (via TemplateFlow — handles its own caching)
|
|
211
|
+
# ---------------------------------------------------------------------------
|
|
212
|
+
|
|
213
|
+
# MNI152NLin2009cAsym (ICBM 2009c, asymmetric — most common)
|
|
214
|
+
mni09c = TFlowTemplate("MNI152 2009c T1w brain", "MNI152NLin2009cAsym", "T1w", desc="brain")
|
|
215
|
+
mni09c_T1w = mni09c # alias
|
|
216
|
+
mni09c_T2w = TFlowTemplate("MNI152 2009c T2w", "MNI152NLin2009cAsym", "T2w")
|
|
217
|
+
mni09c_mask = TFlowTemplate("MNI152 2009c brain mask", "MNI152NLin2009cAsym", "mask", desc="brain")
|
|
218
|
+
|
|
219
|
+
# MNI152NLin2009cSym (ICBM 2009c, symmetric)
|
|
220
|
+
mni09c_sym = TFlowTemplate("MNI152 2009c sym T1w brain", "MNI152NLin2009cSym", "T1w", desc="brain")
|
|
221
|
+
mni09c_sym_T2w = TFlowTemplate("MNI152 2009c sym T2w", "MNI152NLin2009cSym", "T2w")
|
|
222
|
+
|
|
223
|
+
# MNI152NLin6Asym (6th generation — FSL/SPM default)
|
|
224
|
+
mni6 = TFlowTemplate("MNI152 6th gen T1w brain", "MNI152NLin6Asym", "T1w", desc="brain")
|
|
225
|
+
mni6_T1w = mni6 # alias
|
|
226
|
+
mni6_T2w = TFlowTemplate("MNI152 6th gen T2w", "MNI152NLin6Asym", "T2w")
|
|
227
|
+
mni6_mask = TFlowTemplate("MNI152 6th gen brain mask","MNI152NLin6Asym", "mask", desc="brain")
|
|
228
|
+
|
|
229
|
+
# ---------------------------------------------------------------------------
|
|
230
|
+
# BigBrain histological volume (8-bit, MNI ICBM 152 2009b sym)
|
|
231
|
+
# Amunts et al. (2013) Science 340(6139):1472–1475
|
|
232
|
+
# The raw volume has the background at max intensity — postprocess replaces
|
|
233
|
+
# max → NaN so the background is transparent when plotting.
|
|
234
|
+
|
|
235
|
+
def _bigbrain_mask_background(raw_path: Path) -> Path:
|
|
236
|
+
"""Replace background (max intensity) with NaN; cache result on disk."""
|
|
237
|
+
import nibabel as nib
|
|
238
|
+
import numpy as np
|
|
239
|
+
|
|
240
|
+
masked_path = raw_path.with_name(raw_path.name.replace(".nii", "_masked.nii"))
|
|
241
|
+
if masked_path.exists():
|
|
242
|
+
return masked_path
|
|
243
|
+
print(f" Masking BigBrain background (max → NaN) …")
|
|
244
|
+
img = nib.load(raw_path)
|
|
245
|
+
data = img.get_fdata()
|
|
246
|
+
out = np.where(data == data.max(), np.nan, data).astype(np.float32)
|
|
247
|
+
new_img = nib.Nifti1Image(out, img.affine)
|
|
248
|
+
new_img.header.set_data_dtype(np.float32)
|
|
249
|
+
nib.save(new_img, masked_path)
|
|
250
|
+
return masked_path
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
bigbrain = Template("BigBrain", {
|
|
254
|
+
"100um": ("full8_100um_2009b_sym.nii.gz", f"{_BB}/full8_100um_2009b_sym.nii.gz"), # 1.1 GB
|
|
255
|
+
"200um": ("full8_200um_2009b_sym.nii.gz", f"{_BB}/full8_200um_2009b_sym.nii.gz"), # 149 MB
|
|
256
|
+
"300um": ("full8_300um_2009b_sym.nii.gz", f"{_BB}/full8_300um_2009b_sym.nii.gz"), # 46 MB
|
|
257
|
+
"400um": ("full8_400um_2009b_sym.nii.gz", f"{_BB}/full8_400um_2009b_sym.nii.gz"), # 20 MB
|
|
258
|
+
}, default="300um", postprocess=_bigbrain_mask_background)
|
|
259
|
+
|
|
260
|
+
# Synthesized FLASH25 ex-vivo 7T MRI in MNI space (CC0)
|
|
261
|
+
# Edlow et al. (2019) Scientific Data 6:244
|
|
262
|
+
flash25 = Template("FLASH25", {
|
|
263
|
+
"100um": ("Synthesized_FLASH25_in_MNI_v2_100um.nii.gz", f"{_DD}/182487"), # 4.7 GB
|
|
264
|
+
"200um": ("Synthesized_FLASH25_in_MNI_v2_200um.nii.gz", f"{_DD}/182488"), # 1.15 GB
|
|
265
|
+
"500um": ("Synthesized_FLASH25_in_MNI_v2_500um.nii.gz", f"{_DD}/182489"), # 74 MB
|
|
266
|
+
}, default="500um", migrate_legacy=True)
|
|
267
|
+
|
|
268
|
+
# Convenience aliases for backwards compatibility
|
|
269
|
+
get_bigbrain = bigbrain.get
|
|
270
|
+
get_flash25 = flash25.get
|
bsplot/text.py
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import black
|
|
2
|
+
import matplotlib.pyplot as plt
|
|
3
|
+
from pygments.lexers import PythonLexer, get_lexer_by_name
|
|
4
|
+
from pygments.styles import get_style_by_name
|
|
5
|
+
import textwrap
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _wrap_lines(text: str, line_length: int) -> str:
|
|
9
|
+
"""Wrap long lines in text while preserving structure for non-Python code."""
|
|
10
|
+
lines = text.splitlines()
|
|
11
|
+
wrapped_lines = []
|
|
12
|
+
|
|
13
|
+
for line in lines:
|
|
14
|
+
if len(line) <= line_length:
|
|
15
|
+
wrapped_lines.append(line)
|
|
16
|
+
else:
|
|
17
|
+
wrapped = textwrap.fill(
|
|
18
|
+
line,
|
|
19
|
+
width=line_length,
|
|
20
|
+
break_long_words=True,
|
|
21
|
+
break_on_hyphens=True,
|
|
22
|
+
expand_tabs=False,
|
|
23
|
+
replace_whitespace=False,
|
|
24
|
+
)
|
|
25
|
+
wrapped_lines.extend(wrapped.splitlines())
|
|
26
|
+
|
|
27
|
+
return "\n".join(wrapped_lines)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def draw_code(
|
|
31
|
+
ax,
|
|
32
|
+
code: str,
|
|
33
|
+
fontsize=7,
|
|
34
|
+
bg=None,
|
|
35
|
+
style="monokai",
|
|
36
|
+
language: str = "python",
|
|
37
|
+
line_length=60,
|
|
38
|
+
line_height=None,
|
|
39
|
+
char_width=0.015,
|
|
40
|
+
):
|
|
41
|
+
"""Render syntax-highlighted code (Python by default) inside a matplotlib axis.
|
|
42
|
+
|
|
43
|
+
Parameters
|
|
44
|
+
----------
|
|
45
|
+
ax : matplotlib.axes.Axes
|
|
46
|
+
Axis to draw on.
|
|
47
|
+
code : str
|
|
48
|
+
Source code string to render.
|
|
49
|
+
fontsize : int, optional
|
|
50
|
+
Font size for text (default 7).
|
|
51
|
+
bg : str | None, optional
|
|
52
|
+
Background face color. If None, uses the selected Pygments style's
|
|
53
|
+
background color; if unavailable, falls back to the current Matplotlib
|
|
54
|
+
axes facecolor (rcParams["axes.facecolor"]).
|
|
55
|
+
style : str, optional
|
|
56
|
+
Pygments style name (default 'monokai'). See ``pygments.styles.STYLE_MAP``.
|
|
57
|
+
language : str, optional
|
|
58
|
+
Lexer language (``python``, ``yaml``, ``json``, ``bash``, etc.). If ``python``
|
|
59
|
+
the code is first formatted with *black*. For other languages the string is
|
|
60
|
+
used as-is (black is skipped to avoid syntax errors).
|
|
61
|
+
line_length : int, optional
|
|
62
|
+
Maximum line length for formatting (default 60). For Python, this is passed
|
|
63
|
+
to black formatter. For other languages, long lines are wrapped at word
|
|
64
|
+
boundaries when possible.
|
|
65
|
+
line_height : float, optional
|
|
66
|
+
Fixed line height as fraction of axis height (each row's vertical advance).
|
|
67
|
+
If None, the available space between equal top/bottom padding is divided
|
|
68
|
+
equally over the number of rendered lines.
|
|
69
|
+
char_width : float, optional
|
|
70
|
+
Approximate width advance per character (fraction of axis width). If None, defaults to 0.015.
|
|
71
|
+
|
|
72
|
+
Notes
|
|
73
|
+
-----
|
|
74
|
+
- Backwards compatible: existing calls without new parameters behave identically.
|
|
75
|
+
- For long code blocks, consider reducing ``fontsize`` or adjusting ``line_length``.
|
|
76
|
+
"""
|
|
77
|
+
# Setup
|
|
78
|
+
ax.set_xlim(0, 1)
|
|
79
|
+
ax.set_ylim(0, 1)
|
|
80
|
+
|
|
81
|
+
# Resolve Pygments style early (avoid shadowing the style parameter)
|
|
82
|
+
style_name = style
|
|
83
|
+
pyg_style = get_style_by_name(style_name)
|
|
84
|
+
|
|
85
|
+
# Resolve background color preference: Pygments style -> Matplotlib rcParams -> fallback
|
|
86
|
+
if bg is None:
|
|
87
|
+
bg_color = getattr(pyg_style, "background_color", None)
|
|
88
|
+
if not bg_color:
|
|
89
|
+
bg_color = plt.rcParams.get("axes.facecolor", "#ffffff")
|
|
90
|
+
else:
|
|
91
|
+
bg_color = bg
|
|
92
|
+
|
|
93
|
+
ax.add_patch(
|
|
94
|
+
plt.Rectangle((0, 0), 1, 1, facecolor=bg_color, edgecolor=None, linewidth=0.5)
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# Select lexer & format code
|
|
98
|
+
if language.lower() == "python":
|
|
99
|
+
formatted_code = black.format_str(
|
|
100
|
+
code, mode=black.Mode(line_length=line_length)
|
|
101
|
+
)
|
|
102
|
+
lexer = PythonLexer()
|
|
103
|
+
else:
|
|
104
|
+
# Apply line wrapping for non-Python languages
|
|
105
|
+
formatted_code = _wrap_lines(code, line_length)
|
|
106
|
+
lexer = get_lexer_by_name(language.lower())
|
|
107
|
+
|
|
108
|
+
tokens = list(lexer.get_tokens(formatted_code))
|
|
109
|
+
|
|
110
|
+
# Use built-in Pygments style (already resolved as pyg_style)
|
|
111
|
+
|
|
112
|
+
def get_token_color(token_type):
|
|
113
|
+
"""Get hex color for a token using the active Pygments style.
|
|
114
|
+
|
|
115
|
+
Uses normalized style attributes (pyg_style._styles) and walks up the
|
|
116
|
+
token hierarchy. Returns a hex color string or None if unspecified.
|
|
117
|
+
"""
|
|
118
|
+
from pygments.token import Token as PygToken
|
|
119
|
+
|
|
120
|
+
def normalize(col: str | None) -> str | None:
|
|
121
|
+
if not col:
|
|
122
|
+
return None
|
|
123
|
+
return col if col.startswith("#") else f"#{col}"
|
|
124
|
+
|
|
125
|
+
cur = token_type
|
|
126
|
+
while cur is not None:
|
|
127
|
+
attrs = pyg_style._styles.get(cur)
|
|
128
|
+
if attrs:
|
|
129
|
+
col = normalize(attrs[0]) # first field is the foreground color
|
|
130
|
+
if col:
|
|
131
|
+
return col
|
|
132
|
+
cur = cur.parent
|
|
133
|
+
|
|
134
|
+
# Try the generic Token entry
|
|
135
|
+
attrs = pyg_style._styles.get(PygToken)
|
|
136
|
+
if attrs:
|
|
137
|
+
col = normalize(attrs[0])
|
|
138
|
+
if col:
|
|
139
|
+
return col
|
|
140
|
+
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
# Build lines from tokens
|
|
144
|
+
def build_lines(tokens):
|
|
145
|
+
lines, current_line = [], []
|
|
146
|
+
for token_type, value in tokens:
|
|
147
|
+
if "\n" not in value:
|
|
148
|
+
current_line.append((token_type, value))
|
|
149
|
+
else:
|
|
150
|
+
parts = value.split("\n")
|
|
151
|
+
if parts[0]:
|
|
152
|
+
current_line.append((token_type, parts[0]))
|
|
153
|
+
lines.append(current_line)
|
|
154
|
+
|
|
155
|
+
for part in parts[1:-1]:
|
|
156
|
+
lines.append([(token_type, part)] if part else [])
|
|
157
|
+
|
|
158
|
+
current_line = [(token_type, parts[-1])] if parts[-1] else []
|
|
159
|
+
|
|
160
|
+
if current_line:
|
|
161
|
+
lines.append(current_line)
|
|
162
|
+
return lines
|
|
163
|
+
|
|
164
|
+
# Render with adaptive spacing
|
|
165
|
+
lines = build_lines(tokens)
|
|
166
|
+
if not lines:
|
|
167
|
+
return
|
|
168
|
+
|
|
169
|
+
padding = 0.02
|
|
170
|
+
x_start = padding
|
|
171
|
+
y_start = 1 - padding
|
|
172
|
+
|
|
173
|
+
if char_width is None:
|
|
174
|
+
char_width = 0.015
|
|
175
|
+
|
|
176
|
+
if line_height is None:
|
|
177
|
+
usable_height = max(0.0, 1 - 2 * padding)
|
|
178
|
+
row_height = usable_height / len(lines) if len(lines) else usable_height
|
|
179
|
+
else:
|
|
180
|
+
row_height = line_height
|
|
181
|
+
|
|
182
|
+
for i, line_tokens in enumerate(lines):
|
|
183
|
+
y_pos = y_start - i * row_height
|
|
184
|
+
x_pos = x_start
|
|
185
|
+
for token_type, value in line_tokens:
|
|
186
|
+
# Preserve indentation & spaces: still render empty strings for alignment
|
|
187
|
+
if value:
|
|
188
|
+
color = get_token_color(token_type)
|
|
189
|
+
ax.text(
|
|
190
|
+
x_pos,
|
|
191
|
+
y_pos,
|
|
192
|
+
value,
|
|
193
|
+
fontfamily="monospace",
|
|
194
|
+
fontsize=fontsize,
|
|
195
|
+
color=color,
|
|
196
|
+
va="top",
|
|
197
|
+
ha="left",
|
|
198
|
+
)
|
|
199
|
+
x_pos += len(value) * char_width
|
bsplot/text2obj.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from os.path import join, isfile
|
|
5
|
+
|
|
6
|
+
def main(base_path):
|
|
7
|
+
"""
|
|
8
|
+
Converts vertex, normal, and triangle data into an OBJ file format.
|
|
9
|
+
|
|
10
|
+
Parameters:
|
|
11
|
+
base_path (str): The base directory containing vertices.txt, normals.txt, and triangles.txt.
|
|
12
|
+
|
|
13
|
+
Outputs:
|
|
14
|
+
surface.obj file in the specified directory.
|
|
15
|
+
"""
|
|
16
|
+
vertices_file = join(base_path, "vertices.txt")
|
|
17
|
+
normals_file = join(base_path, "normals.txt")
|
|
18
|
+
triangles_file = join(base_path, "triangles.txt")
|
|
19
|
+
output_file = join(base_path, "surface.obj")
|
|
20
|
+
|
|
21
|
+
# Check if normals file exists
|
|
22
|
+
normals_exist = isfile(normals_file)
|
|
23
|
+
|
|
24
|
+
# Reading vertices and triangles
|
|
25
|
+
with open(vertices_file, "r") as vf, open(triangles_file, "r") as tf:
|
|
26
|
+
vertices = [line.strip().split() for line in vf]
|
|
27
|
+
triangles = [line.strip().split() for line in tf]
|
|
28
|
+
|
|
29
|
+
# Read normals only if they exist
|
|
30
|
+
if normals_exist:
|
|
31
|
+
with open(normals_file, "r") as nf:
|
|
32
|
+
normals = [line.strip().split() for line in nf]
|
|
33
|
+
|
|
34
|
+
# Writing to OBJ format
|
|
35
|
+
with open(output_file, "w") as of:
|
|
36
|
+
# Write vertices
|
|
37
|
+
for v in vertices:
|
|
38
|
+
of.write(f"v {' '.join(v)}\n")
|
|
39
|
+
|
|
40
|
+
# Write normals if available
|
|
41
|
+
if normals_exist:
|
|
42
|
+
for n in normals:
|
|
43
|
+
of.write(f"vn {' '.join(n)}\n")
|
|
44
|
+
|
|
45
|
+
# Write faces with or without normals
|
|
46
|
+
for t in triangles:
|
|
47
|
+
indices = [str(int(idx) + 1) for idx in t]
|
|
48
|
+
if normals_exist:
|
|
49
|
+
of.write(
|
|
50
|
+
f"f {indices[0]}//{indices[0]} {indices[1]}//{indices[1]} {indices[2]}//{indices[2]}\n"
|
|
51
|
+
)
|
|
52
|
+
else:
|
|
53
|
+
of.write(f"f {indices[0]} {indices[1]} {indices[2]}\n")
|
|
54
|
+
|
|
55
|
+
if __name__ == "__main__":
|
|
56
|
+
if len(sys.argv) != 2:
|
|
57
|
+
print("Usage: python text2obj.py <base_path>")
|
|
58
|
+
else:
|
|
59
|
+
main(sys.argv[1])
|
bsplot/timeseries.py
ADDED
bsplot/utils.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Utility helpers for plotting and figure management."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def autoscale_axis(ax):
|
|
5
|
+
"""Recompute axis limits after adding artists like LineCollection.
|
|
6
|
+
|
|
7
|
+
Useful when plotting line collections, which do not always update limits
|
|
8
|
+
automatically. Call this only when you want the axis limits to be reset
|
|
9
|
+
from current artists.
|
|
10
|
+
"""
|
|
11
|
+
ax.autoscale(enable=True)
|
|
12
|
+
ax.autoscale_view()
|
|
13
|
+
return ax
|