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/data/surface.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
from os.path import abspath, dirname, join
|
|
2
|
+
|
|
3
|
+
import nibabel as nib
|
|
4
|
+
import templateflow
|
|
5
|
+
|
|
6
|
+
root = dirname(abspath(__file__))
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_flat_surface_geometry(template="fsaverage", hemi="lh"):
|
|
10
|
+
"""Load a cortical flatmap surface (cut and unfolded mesh).
|
|
11
|
+
|
|
12
|
+
Currently available for:
|
|
13
|
+
- ``fsaverage`` (164k vertices) via nilearn
|
|
14
|
+
|
|
15
|
+
Parameters
|
|
16
|
+
----------
|
|
17
|
+
template : str
|
|
18
|
+
Template name. Only ``"fsaverage"`` is supported.
|
|
19
|
+
hemi : str
|
|
20
|
+
Hemisphere: ``"lh"`` / ``"left"`` / ``"L"`` or
|
|
21
|
+
``"rh"`` / ``"right"`` / ``"R"``.
|
|
22
|
+
|
|
23
|
+
Returns
|
|
24
|
+
-------
|
|
25
|
+
vertices : ndarray, shape (n_vertices, 3)
|
|
26
|
+
faces : ndarray, shape (n_faces, 3)
|
|
27
|
+
"""
|
|
28
|
+
from nilearn import datasets
|
|
29
|
+
|
|
30
|
+
if hemi.lower() in ("lh", "left", "l"):
|
|
31
|
+
side = "left"
|
|
32
|
+
elif hemi.lower() in ("rh", "right", "r"):
|
|
33
|
+
side = "right"
|
|
34
|
+
else:
|
|
35
|
+
raise ValueError(f"Unknown hemisphere '{hemi}'")
|
|
36
|
+
|
|
37
|
+
tpl = template.lower()
|
|
38
|
+
import contextlib, io
|
|
39
|
+
with contextlib.redirect_stdout(io.StringIO()):
|
|
40
|
+
if tpl == "fsaverage":
|
|
41
|
+
fsavg = datasets.fetch_surf_fsaverage(mesh="fsaverage")
|
|
42
|
+
elif tpl in ("fsaverage5", "fsaverage_5"):
|
|
43
|
+
fsavg = datasets.fetch_surf_fsaverage(mesh="fsaverage5")
|
|
44
|
+
else:
|
|
45
|
+
raise ValueError(
|
|
46
|
+
f"Flat surface not available for template '{template}'. "
|
|
47
|
+
"Supported: 'fsaverage', 'fsaverage5'."
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
flat_path = fsavg[f"flat_{side}"]
|
|
51
|
+
gii = nib.load(flat_path)
|
|
52
|
+
vertices, faces = gii.agg_data()
|
|
53
|
+
return vertices, faces
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def get_surface_geometry(
|
|
57
|
+
template="fsLR",
|
|
58
|
+
suffix="midthickness",
|
|
59
|
+
hemi="lh",
|
|
60
|
+
density="32k",
|
|
61
|
+
desc=None,
|
|
62
|
+
extension="surf.gii",
|
|
63
|
+
):
|
|
64
|
+
|
|
65
|
+
if template == "fsLR":
|
|
66
|
+
if suffix not in [
|
|
67
|
+
"inflated",
|
|
68
|
+
"midthickness",
|
|
69
|
+
"veryinflated",
|
|
70
|
+
"sphere",
|
|
71
|
+
]:
|
|
72
|
+
raise ValueError(
|
|
73
|
+
f"Invalid suffix '{suffix}' for template 'fsLR'. Must be one of 'inflated', 'midthickness', 'veryinflated', or 'sphere'."
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
elif template == "fsaverage":
|
|
77
|
+
if suffix not in [
|
|
78
|
+
"inflated",
|
|
79
|
+
"midthickness",
|
|
80
|
+
"pial",
|
|
81
|
+
"smoothwm",
|
|
82
|
+
"sphere",
|
|
83
|
+
"white",
|
|
84
|
+
]:
|
|
85
|
+
raise ValueError(
|
|
86
|
+
f"Invalid suffix '{suffix}' for template 'fsaverage'. Must be one of 'inflated', 'midthickness', 'pial', 'smoothwm', 'sphere', or 'white'."
|
|
87
|
+
)
|
|
88
|
+
if density not in ["164k", "41k", "10k", "3k"]:
|
|
89
|
+
raise ValueError(
|
|
90
|
+
f"Invalid density '{density}' for template 'fsaverage'. Must be one of '164k', '41k', '10k', or '3k'."
|
|
91
|
+
)
|
|
92
|
+
if hemi in ["lh", "left"]:
|
|
93
|
+
hemi = "L"
|
|
94
|
+
elif hemi in ["rh", "right"]:
|
|
95
|
+
hemi = "R"
|
|
96
|
+
|
|
97
|
+
fsLR = templateflow.api.get(
|
|
98
|
+
template=template,
|
|
99
|
+
density=density,
|
|
100
|
+
suffix=suffix,
|
|
101
|
+
hemi=hemi,
|
|
102
|
+
desc=desc,
|
|
103
|
+
extension=extension,
|
|
104
|
+
)
|
|
105
|
+
# print(f"Loading surface geometry from {fsLR}")
|
|
106
|
+
if isinstance(fsLR, list):
|
|
107
|
+
raise ValueError(f"Expected a single file path, but got a list: {fsLR}")
|
|
108
|
+
gii = nib.load(fsLR)
|
|
109
|
+
|
|
110
|
+
vertices, triangles = gii.agg_data()
|
|
111
|
+
|
|
112
|
+
return vertices, triangles
|
|
Binary file
|
bsplot/figure.py
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Figure layout wizard for bsplot.
|
|
3
|
+
|
|
4
|
+
Provides a convenient API for creating matplotlib figures with labeled axes
|
|
5
|
+
using ``subplot_mosaic`` under the hood.
|
|
6
|
+
|
|
7
|
+
Examples
|
|
8
|
+
--------
|
|
9
|
+
Simple single-panel figure:
|
|
10
|
+
|
|
11
|
+
>>> fig, ax = bsplot.figure.subplots(layout="a")
|
|
12
|
+
|
|
13
|
+
Row of three panels:
|
|
14
|
+
|
|
15
|
+
>>> fig, ax = bsplot.figure.subplots(layout="abc")
|
|
16
|
+
|
|
17
|
+
2×3 grid with mosaic layout:
|
|
18
|
+
|
|
19
|
+
>>> fig, ax = bsplot.figure.subplots(layout="abc\\ndef")
|
|
20
|
+
|
|
21
|
+
Spanning cells (matplotlib mosaic syntax):
|
|
22
|
+
|
|
23
|
+
>>> fig, ax = bsplot.figure.subplots(layout="aab\\nccd")
|
|
24
|
+
|
|
25
|
+
Access axes by label:
|
|
26
|
+
|
|
27
|
+
>>> ax["a"].plot([1, 2, 3])
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import string
|
|
33
|
+
from typing import Any
|
|
34
|
+
|
|
35
|
+
import matplotlib.pyplot as plt
|
|
36
|
+
import matplotlib.axes
|
|
37
|
+
import matplotlib.figure
|
|
38
|
+
import numpy as np
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _parse_layout(layout):
|
|
42
|
+
"""Convert a compact layout string into the list-of-lists expected by
|
|
43
|
+
``subplot_mosaic``.
|
|
44
|
+
|
|
45
|
+
Parameters
|
|
46
|
+
----------
|
|
47
|
+
layout : str or list[list[str]]
|
|
48
|
+
If a string, each character becomes an axes label. Rows are
|
|
49
|
+
separated by ``\\n``. Spaces and ``|`` are treated as column
|
|
50
|
+
separators for readability but are otherwise ignored.
|
|
51
|
+
A ``.`` marks an empty cell.
|
|
52
|
+
If already a list-of-lists, it is returned as-is.
|
|
53
|
+
|
|
54
|
+
Returns
|
|
55
|
+
-------
|
|
56
|
+
list[list[str]]
|
|
57
|
+
Mosaic specification suitable for ``plt.subplot_mosaic``.
|
|
58
|
+
"""
|
|
59
|
+
if isinstance(layout, list):
|
|
60
|
+
return layout
|
|
61
|
+
|
|
62
|
+
rows = layout.split("\n")
|
|
63
|
+
mosaic = []
|
|
64
|
+
for row in rows:
|
|
65
|
+
# strip leading/trailing whitespace per row
|
|
66
|
+
row = row.strip()
|
|
67
|
+
if not row:
|
|
68
|
+
continue
|
|
69
|
+
# allow pipe separators for readability (e.g. "a|b|c")
|
|
70
|
+
row = row.replace("|", "")
|
|
71
|
+
# each remaining character is a cell
|
|
72
|
+
mosaic.append(list(row))
|
|
73
|
+
return mosaic
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def subplots(
|
|
77
|
+
layout: str | list[list[str]] = "a",
|
|
78
|
+
*,
|
|
79
|
+
figsize: tuple[float, float] | None = None,
|
|
80
|
+
dpi: float | None = None,
|
|
81
|
+
width_ratios: list[float] | None = None,
|
|
82
|
+
height_ratios: list[float] | None = None,
|
|
83
|
+
gridspec_kw: dict | None = None,
|
|
84
|
+
subplot_kw: dict | None = None,
|
|
85
|
+
per_subplot_kw: dict[str, dict[str, Any]] | None = None,
|
|
86
|
+
constrained_layout: bool = True,
|
|
87
|
+
**fig_kw,
|
|
88
|
+
) -> tuple[matplotlib.figure.Figure, dict[str, matplotlib.axes.Axes]]:
|
|
89
|
+
"""Create a figure with labeled subplots using mosaic layout.
|
|
90
|
+
|
|
91
|
+
This is bsplot's main entry-point for building multi-panel figures.
|
|
92
|
+
Under the hood it delegates to :func:`matplotlib.pyplot.subplot_mosaic`,
|
|
93
|
+
so every feature of the mosaic API is available.
|
|
94
|
+
|
|
95
|
+
Parameters
|
|
96
|
+
----------
|
|
97
|
+
layout : str or list[list[str]], default ``"a"``
|
|
98
|
+
Compact layout specification.
|
|
99
|
+
|
|
100
|
+
* A plain string like ``"abc"`` creates a single row of panels
|
|
101
|
+
labeled *a*, *b*, *c*.
|
|
102
|
+
* Use ``\\n`` to separate rows: ``"ab\\ncd"`` gives a 2×2 grid.
|
|
103
|
+
* A ``.`` marks an empty cell.
|
|
104
|
+
* Repeating a letter spans that panel across cells:
|
|
105
|
+
``"aab\\nccd"`` makes *a* span two columns in the first row.
|
|
106
|
+
* You can also pass a nested list directly for full control.
|
|
107
|
+
|
|
108
|
+
figsize : tuple of float, optional
|
|
109
|
+
Figure size ``(width, height)`` in inches. When *None* a
|
|
110
|
+
sensible default is chosen based on the number of rows and
|
|
111
|
+
columns.
|
|
112
|
+
dpi : float, optional
|
|
113
|
+
Resolution. Defaults to matplotlib's ``figure.dpi`` rcParam.
|
|
114
|
+
width_ratios : list of float, optional
|
|
115
|
+
Relative widths of each column.
|
|
116
|
+
height_ratios : list of float, optional
|
|
117
|
+
Relative heights of each row.
|
|
118
|
+
gridspec_kw : dict, optional
|
|
119
|
+
Extra keyword arguments forwarded to the underlying
|
|
120
|
+
:class:`~matplotlib.gridspec.GridSpec`.
|
|
121
|
+
subplot_kw : dict, optional
|
|
122
|
+
Keyword arguments applied to **every** subplot
|
|
123
|
+
(e.g. ``{"projection": "polar"}``).
|
|
124
|
+
per_subplot_kw : dict, optional
|
|
125
|
+
Per-label keyword arguments, e.g.
|
|
126
|
+
``{"a": {"projection": "polar"}}``.
|
|
127
|
+
constrained_layout : bool, default True
|
|
128
|
+
Use matplotlib's constrained layout engine for automatic spacing.
|
|
129
|
+
**fig_kw
|
|
130
|
+
Any remaining keyword arguments are forwarded to
|
|
131
|
+
:func:`matplotlib.pyplot.figure`.
|
|
132
|
+
|
|
133
|
+
Returns
|
|
134
|
+
-------
|
|
135
|
+
fig : matplotlib.figure.Figure
|
|
136
|
+
ax : dict[str, matplotlib.axes.Axes]
|
|
137
|
+
A dict keyed by the panel label strings.
|
|
138
|
+
|
|
139
|
+
Examples
|
|
140
|
+
--------
|
|
141
|
+
>>> fig, ax = bsplot.figure.subplots() # single panel
|
|
142
|
+
>>> fig, ax = bsplot.figure.subplots("abc") # 1×3 row
|
|
143
|
+
>>> fig, ax = bsplot.figure.subplots("ab\\ncd") # 2×2 grid
|
|
144
|
+
>>> fig, ax = bsplot.figure.subplots("aab\\n.cd") # mosaic
|
|
145
|
+
>>> ax["a"].plot([0, 1], [0, 1])
|
|
146
|
+
"""
|
|
147
|
+
mosaic = _parse_layout(layout)
|
|
148
|
+
|
|
149
|
+
# --- derive a default figsize from the grid dimensions ---------------
|
|
150
|
+
nrows = len(mosaic)
|
|
151
|
+
ncols = max(len(row) for row in mosaic) if mosaic else 1
|
|
152
|
+
if figsize is None:
|
|
153
|
+
col_width = 4.0
|
|
154
|
+
row_height = 3.5
|
|
155
|
+
figsize = (ncols * col_width, nrows * row_height)
|
|
156
|
+
|
|
157
|
+
# --- assemble gridspec_kw --------------------------------------------
|
|
158
|
+
if gridspec_kw is None:
|
|
159
|
+
gridspec_kw = {}
|
|
160
|
+
if width_ratios is not None:
|
|
161
|
+
gridspec_kw["width_ratios"] = width_ratios
|
|
162
|
+
if height_ratios is not None:
|
|
163
|
+
gridspec_kw["height_ratios"] = height_ratios
|
|
164
|
+
|
|
165
|
+
# --- create the figure -----------------------------------------------
|
|
166
|
+
fig, ax_dict = plt.subplot_mosaic(
|
|
167
|
+
mosaic, # type: ignore[arg-type]
|
|
168
|
+
figsize=figsize,
|
|
169
|
+
dpi=dpi,
|
|
170
|
+
gridspec_kw=gridspec_kw or None,
|
|
171
|
+
subplot_kw=subplot_kw,
|
|
172
|
+
per_subplot_kw=per_subplot_kw, # type: ignore[arg-type]
|
|
173
|
+
layout="constrained" if constrained_layout else None,
|
|
174
|
+
**fig_kw,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
return fig, ax_dict
|
bsplot/graph/__init__.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from . import edges, layout, nodes
|
|
2
|
+
from .flowchart import (
|
|
3
|
+
build_dot,
|
|
4
|
+
flowchart_html,
|
|
5
|
+
prune_nodes_to_edges,
|
|
6
|
+
render_html,
|
|
7
|
+
save_html,
|
|
8
|
+
show_matplotlib,
|
|
9
|
+
show_matplotlib_bioicons,
|
|
10
|
+
)
|
|
11
|
+
from .network import (
|
|
12
|
+
get_centers_from_surface_parc,
|
|
13
|
+
create_network,
|
|
14
|
+
create_node_mesh,
|
|
15
|
+
create_edge_mesh,
|
|
16
|
+
plot_network_on_surface,
|
|
17
|
+
)
|
|
18
|
+
from .nodes import (
|
|
19
|
+
draw_custom_nodes,
|
|
20
|
+
draw_nodes_with_icons,
|
|
21
|
+
draw_icon_nodes,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# Re-export from parent for convenience
|
|
25
|
+
from ..bioicons import (
|
|
26
|
+
BioIcons,
|
|
27
|
+
plot_icon,
|
|
28
|
+
plot_icons_grid,
|
|
29
|
+
IconAnnotation,
|
|
30
|
+
search_icons,
|
|
31
|
+
)
|
bsplot/graph/edges.py
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from matplotlib.colors import Normalize
|
|
3
|
+
from bsplot.colors import Colormap as _resolve_cmap
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
from matplotlib.colors import Normalize
|
|
7
|
+
from matplotlib.patches import FancyArrowPatch
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_edge_color_mapping(G, colormap="viridis", color_by="type"):
|
|
11
|
+
"""
|
|
12
|
+
Generate a color mapping for edges in graph G based on their specified attribute.
|
|
13
|
+
|
|
14
|
+
Parameters:
|
|
15
|
+
----------
|
|
16
|
+
G : networkx.Graph
|
|
17
|
+
The graph containing the edges with the specified attribute.
|
|
18
|
+
colormap : str, optional
|
|
19
|
+
The name of the colormap to use (default is 'viridis').
|
|
20
|
+
color_by : str, optional
|
|
21
|
+
The edge attribute to color by (default is 'type').
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
-------
|
|
25
|
+
edge_colors : dict
|
|
26
|
+
A dictionary mapping edge attributes to colors.
|
|
27
|
+
"""
|
|
28
|
+
# Extract all edge attributes for the specified attribute
|
|
29
|
+
edge_attributes = [
|
|
30
|
+
data[color_by] if color_by in data.keys() else "n.a."
|
|
31
|
+
for u, v, data in G.edges(data=True)
|
|
32
|
+
]
|
|
33
|
+
unique_edge_attributes = list(set(edge_attributes))
|
|
34
|
+
|
|
35
|
+
# Ensure "n.a." is first in the list
|
|
36
|
+
if "n.a." in unique_edge_attributes:
|
|
37
|
+
unique_edge_attributes.remove("n.a.")
|
|
38
|
+
unique_edge_attributes.insert(0, "n.a.")
|
|
39
|
+
|
|
40
|
+
# Create a colormap
|
|
41
|
+
cmap = _resolve_cmap(colormap)
|
|
42
|
+
norm = Normalize(vmin=0, vmax=len(unique_edge_attributes) - 1)
|
|
43
|
+
|
|
44
|
+
# Map edge attributes to colors
|
|
45
|
+
edge_colors = {
|
|
46
|
+
attr: cmap(norm(i)) if attr != "n.a." else (0.6, 0.6, 0.6, 1)
|
|
47
|
+
for i, attr in enumerate(unique_edge_attributes)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return edge_colors
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def get_unique_node_pairs(G):
|
|
54
|
+
unique_pairs = set() # Use a set to store unique node pairs
|
|
55
|
+
for n1, n2 in G.edges():
|
|
56
|
+
# Sort using the 'name' attribute of the nodes (assuming each node has a 'name' attribute)
|
|
57
|
+
pair = tuple(
|
|
58
|
+
sorted(
|
|
59
|
+
[n1, n2], key=lambda node: node if isinstance(node, str) else node.name
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
unique_pairs.add(pair) # Add the sorted pair to the set
|
|
63
|
+
return unique_pairs
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def compute_midpoint(start, end, rad):
|
|
67
|
+
"""
|
|
68
|
+
Compute the midpoint of a quadratic Bézier curve.
|
|
69
|
+
|
|
70
|
+
Parameters:
|
|
71
|
+
- start: tuple of (x, y) coordinates of the start point.
|
|
72
|
+
- end: tuple of (x, y) coordinates of the end point.
|
|
73
|
+
- rad: curvature radius as a percentage.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
- tuple of (x, y) coordinates of the midpoint.
|
|
77
|
+
"""
|
|
78
|
+
x1, y1 = start
|
|
79
|
+
x2, y2 = end
|
|
80
|
+
y12 = (y1 + y2) / 2
|
|
81
|
+
dy = y2 - y1
|
|
82
|
+
cy = y12 + rad * dy
|
|
83
|
+
|
|
84
|
+
# Ensure cy is a single value
|
|
85
|
+
if isinstance(cy, np.ndarray):
|
|
86
|
+
cy = cy.item()
|
|
87
|
+
|
|
88
|
+
# Midpoint of the quadratic Bézier curve
|
|
89
|
+
t = 0.5
|
|
90
|
+
midpoint_x = (1 - t) ** 2 * x1 + 2 * (1 - t) * t * (x1 + (x2 - x1) / 2) + t**2 * x2
|
|
91
|
+
midpoint_y = (1 - t) ** 2 * y1 + 2 * (1 - t) * t * cy + t**2 * y2
|
|
92
|
+
return (midpoint_x, midpoint_y)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _bezier_point(x1, y1, x2, y2, cy, t):
|
|
96
|
+
"""Quadratic Bézier point at parameter t. Control point is ((x1+x2)/2, cy)."""
|
|
97
|
+
cx = (x1 + x2) / 2.0
|
|
98
|
+
bx = (1 - t)**2 * x1 + 2 * (1 - t) * t * cx + t**2 * x2
|
|
99
|
+
by = (1 - t)**2 * y1 + 2 * (1 - t) * t * cy + t**2 * y2
|
|
100
|
+
return float(bx), float(by)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _bezier_tangent(x1, y1, x2, y2, cy, t):
|
|
104
|
+
"""Tangent vector of quadratic Bézier at t (not normalised)."""
|
|
105
|
+
cx = (x1 + x2) / 2.0
|
|
106
|
+
tx = 2 * (1 - t) * (cx - x1) + 2 * t * (x2 - cx)
|
|
107
|
+
ty = 2 * (1 - t) * (cy - y1) + 2 * t * (y2 - cy)
|
|
108
|
+
return float(tx), float(ty)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def plot_curve(
|
|
112
|
+
ax,
|
|
113
|
+
start,
|
|
114
|
+
end,
|
|
115
|
+
rad,
|
|
116
|
+
color="black",
|
|
117
|
+
shrinkA=0,
|
|
118
|
+
shrinkB=0.03,
|
|
119
|
+
arrow_size=0.1,
|
|
120
|
+
arrow_style="->",
|
|
121
|
+
annotate=None,
|
|
122
|
+
label_t=0.5,
|
|
123
|
+
**kwargs,
|
|
124
|
+
):
|
|
125
|
+
font_size = kwargs.pop("font_size") if "font_size" in kwargs else 12
|
|
126
|
+
|
|
127
|
+
x1, y1 = start
|
|
128
|
+
x2, y2 = end
|
|
129
|
+
y12 = (y1 + y2) / 2
|
|
130
|
+
dy = y2 - y1
|
|
131
|
+
cy = y12 + rad * dy
|
|
132
|
+
midpoint = compute_midpoint(start, end, rad)
|
|
133
|
+
|
|
134
|
+
tau = np.linspace(0, 1, 100)
|
|
135
|
+
xsupport = np.linspace(x1, x2, 100)
|
|
136
|
+
ysupport = [(1 - i) ** 2 * y1 + 2 * (1 - i) * i * cy + i**2 * y2 for i in tau]
|
|
137
|
+
|
|
138
|
+
# Apply shrink to the start and end
|
|
139
|
+
shrink_index_start = int(len(xsupport) * shrinkA)
|
|
140
|
+
shrink_index_end = int(len(xsupport) * (1 - shrinkB))
|
|
141
|
+
|
|
142
|
+
line = ax.plot(
|
|
143
|
+
xsupport[shrink_index_start:shrink_index_end],
|
|
144
|
+
ysupport[shrink_index_start:shrink_index_end],
|
|
145
|
+
color=color,
|
|
146
|
+
**kwargs,
|
|
147
|
+
)
|
|
148
|
+
line = line[0]
|
|
149
|
+
if isinstance(annotate, str):
|
|
150
|
+
# Label position: Bézier point at label_t
|
|
151
|
+
lp_x, lp_y = _bezier_point(x1, y1, x2, y2, cy, label_t)
|
|
152
|
+
|
|
153
|
+
# True tangent at label_t — varies with both rad and t
|
|
154
|
+
tx, ty = _bezier_tangent(x1, y1, x2, y2, cy, label_t)
|
|
155
|
+
length = np.sqrt(tx**2 + ty**2)
|
|
156
|
+
if length > 0:
|
|
157
|
+
tx /= length
|
|
158
|
+
ty /= length
|
|
159
|
+
else:
|
|
160
|
+
tx, ty = 1.0, 0.0
|
|
161
|
+
|
|
162
|
+
# Right-perpendicular of tangent (rotate 90° clockwise)
|
|
163
|
+
right_x = ty
|
|
164
|
+
right_y = -tx
|
|
165
|
+
|
|
166
|
+
chord_len = np.sqrt((x2 - x1)**2 + (y2 - y1)**2)
|
|
167
|
+
offset = max(0.08 * chord_len, 0.04)
|
|
168
|
+
ax.annotate(
|
|
169
|
+
annotate,
|
|
170
|
+
xy=(lp_x, lp_y),
|
|
171
|
+
xytext=(lp_x + offset * right_x, lp_y + offset * right_y),
|
|
172
|
+
xycoords="data",
|
|
173
|
+
ha="left",
|
|
174
|
+
va="center",
|
|
175
|
+
color="k",
|
|
176
|
+
fontsize=font_size,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def draw_custom_edges(
|
|
181
|
+
G,
|
|
182
|
+
pos,
|
|
183
|
+
ax=None,
|
|
184
|
+
edge_labels=False,
|
|
185
|
+
color_by="type",
|
|
186
|
+
edge_colors="#606060",
|
|
187
|
+
edge_radius=0,
|
|
188
|
+
**kwargs,
|
|
189
|
+
):
|
|
190
|
+
# if "shrinkA" not in kwargs.keys():
|
|
191
|
+
# kwargs["shrinkA"] = .1
|
|
192
|
+
# if "shrinkB" not in kwargs.keys():
|
|
193
|
+
# kwargs["shrinkB"] = .3
|
|
194
|
+
# if "arrowstyle" not in kwargs.keys():
|
|
195
|
+
# kwargs["arrowstyle"] = "-|>,head_length=0.4,head_width=0.2"
|
|
196
|
+
# if "mutation_scale" not in kwargs.keys():
|
|
197
|
+
# kwargs["mutation_scale"] = 10.0
|
|
198
|
+
|
|
199
|
+
if edge_colors in colormaps.keys():
|
|
200
|
+
cmap = (
|
|
201
|
+
edge_colors
|
|
202
|
+
if isinstance(edge_colors, str) and edge_colors in colormaps.keys()
|
|
203
|
+
else "viridis"
|
|
204
|
+
)
|
|
205
|
+
colmap = get_edge_color_mapping(G, colormap=cmap, color_by=color_by)
|
|
206
|
+
edge_colors = [
|
|
207
|
+
colmap[data[color_by] if color_by in data else "n.a."]
|
|
208
|
+
for (u, v, data) in G.edges(data=True)
|
|
209
|
+
]
|
|
210
|
+
elif isinstance(edge_colors, str):
|
|
211
|
+
edge_colors = np.repeat(edge_colors, len(G.edges()))
|
|
212
|
+
|
|
213
|
+
for n1, n2 in get_unique_node_pairs(G):
|
|
214
|
+
point1 = pos[n1]
|
|
215
|
+
point2 = pos[n2]
|
|
216
|
+
types12 = {e["type"] for e in G[n1][n2].values()} if n2 in G[n1] else set()
|
|
217
|
+
types21 = {e["type"] for e in G[n2][n1].values()} if n1 in G[n2] else set()
|
|
218
|
+
|
|
219
|
+
num_edges = len(types12) + len(types21)
|
|
220
|
+
|
|
221
|
+
if num_edges == 1:
|
|
222
|
+
rad = [edge_radius]
|
|
223
|
+
else:
|
|
224
|
+
rad = np.linspace(-0.5, 0.5, num_edges)
|
|
225
|
+
|
|
226
|
+
# Spread label positions along the curve so labels don't stack
|
|
227
|
+
if num_edges == 1:
|
|
228
|
+
label_ts = [0.5]
|
|
229
|
+
else:
|
|
230
|
+
label_ts = np.linspace(0.3, 0.7, num_edges).tolist()
|
|
231
|
+
|
|
232
|
+
i = 0
|
|
233
|
+
for t in types12:
|
|
234
|
+
plot_curve(
|
|
235
|
+
ax,
|
|
236
|
+
point1,
|
|
237
|
+
point2,
|
|
238
|
+
rad[i],
|
|
239
|
+
color=edge_colors[i],
|
|
240
|
+
annotate=t if edge_labels else None,
|
|
241
|
+
label_t=label_ts[i],
|
|
242
|
+
**kwargs,
|
|
243
|
+
)
|
|
244
|
+
i += 1
|
|
245
|
+
for t in types21:
|
|
246
|
+
plot_curve(
|
|
247
|
+
ax,
|
|
248
|
+
point2,
|
|
249
|
+
point1,
|
|
250
|
+
rad[i],
|
|
251
|
+
color=edge_colors[i],
|
|
252
|
+
annotate=t if edge_labels else None,
|
|
253
|
+
label_t=label_ts[i],
|
|
254
|
+
**kwargs,
|
|
255
|
+
)
|
|
256
|
+
i += 1
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def adjust_arrow_end(start, end, bbox_end):
|
|
260
|
+
x_start, y_start = start
|
|
261
|
+
x_end, y_end = end
|
|
262
|
+
|
|
263
|
+
x_min_end, y_min_end, width_end, height_end = bbox_end.bounds
|
|
264
|
+
x_max_end, y_max_end = x_min_end + width_end, y_min_end + height_end
|
|
265
|
+
|
|
266
|
+
# x_min_end, y_min_end = bbox_end[0]
|
|
267
|
+
# x_max_end, y_max_end = bbox_end[1]
|
|
268
|
+
|
|
269
|
+
# Determine which edge of the bounding box to intersect with
|
|
270
|
+
if y_start > y_end: # Arrow goes downward; intersect with top edge
|
|
271
|
+
y_bbox = y_max_end
|
|
272
|
+
else: # Arrow goes upward; intersect with bottom edge
|
|
273
|
+
y_bbox = y_min_end
|
|
274
|
+
|
|
275
|
+
# Calculate the intersection point
|
|
276
|
+
if x_start == x_end: # Vertical line case
|
|
277
|
+
x_intersect = x_start
|
|
278
|
+
else:
|
|
279
|
+
slope = (y_end - y_start) / (x_end - x_start)
|
|
280
|
+
x_intersect = (y_bbox - y_start) / slope + x_start
|
|
281
|
+
|
|
282
|
+
# Clip x_intersect to within the bounding box's horizontal bounds
|
|
283
|
+
x_intersect = max(x_min_end, min(x_intersect, x_max_end))
|
|
284
|
+
|
|
285
|
+
return (x_intersect, y_bbox)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def plot_edge(edge, pos, bbox_positions_data, ax, **kwargs):
|
|
289
|
+
start, end = pos[edge[0]], pos[edge[1]]
|
|
290
|
+
xstart, ystart = start
|
|
291
|
+
xend, yend = end
|
|
292
|
+
|
|
293
|
+
# bbox_start = bbox_positions_data[edge[0]].bounds
|
|
294
|
+
bbox_end = bbox_positions_data[edge[1]]
|
|
295
|
+
|
|
296
|
+
# x_min_start, y_min_start = bbox_start[0]
|
|
297
|
+
# x_max_start, y_max_start = bbox_start[1]
|
|
298
|
+
|
|
299
|
+
# x_min_end, y_min_end = bbox_end[0]
|
|
300
|
+
# x_max_end, y_max_end = bbox_end[1]
|
|
301
|
+
|
|
302
|
+
start = pos[edge[0]]
|
|
303
|
+
end = pos[edge[1]]
|
|
304
|
+
adjusted_end = adjust_arrow_end(start, end, bbox_end)
|
|
305
|
+
|
|
306
|
+
# Plot the edge with adjusted endpoint
|
|
307
|
+
arrow = FancyArrowPatch(
|
|
308
|
+
start,
|
|
309
|
+
adjusted_end,
|
|
310
|
+
**kwargs,
|
|
311
|
+
)
|
|
312
|
+
ax.add_patch(arrow)
|