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.
Files changed (89) hide show
  1. bsplot/__init__.py +23 -0
  2. bsplot/anat.py +55 -0
  3. bsplot/animate.py +194 -0
  4. bsplot/bioicons.py +777 -0
  5. bsplot/brain.py +188 -0
  6. bsplot/colors.py +1935 -0
  7. bsplot/data/HCP_avg-SC.txt +379 -0
  8. bsplot/data/MNI152.rh.pial +0 -0
  9. bsplot/data/STN_lh.nii.gz +0 -0
  10. bsplot/data/STN_rh.nii.gz +0 -0
  11. bsplot/data/__init__.py +22 -0
  12. bsplot/data/left_electrode.ply +257270 -0
  13. bsplot/data/parcellations/HCP-MMP1.L.label.gii +442 -0
  14. bsplot/data/parcellations/HCP-MMP1.R.label.gii +442 -0
  15. bsplot/data/parcellations/code/split_hcpmmp1.sh +4 -0
  16. bsplot/data/right_electrode.ply +257270 -0
  17. bsplot/data/surface.py +112 -0
  18. bsplot/data/tpl-MNI152NLin2009b_atlas-hcpmmp1_desc-ordered_dseg.nii.gz +0 -0
  19. bsplot/figure.py +177 -0
  20. bsplot/graph/__init__.py +31 -0
  21. bsplot/graph/edges.py +312 -0
  22. bsplot/graph/flowchart.py +762 -0
  23. bsplot/graph/layout.py +201 -0
  24. bsplot/graph/network.py +974 -0
  25. bsplot/graph/nodes.py +452 -0
  26. bsplot/panels.py +110 -0
  27. bsplot/scientific_color_maps/__init__.py +0 -0
  28. bsplot/scientific_color_maps/_sync_from_zenodo_download.sh +1 -0
  29. bsplot/scientific_color_maps/batlowK.txt +256 -0
  30. bsplot/scientific_color_maps/batlowW.txt +256 -0
  31. bsplot/scientific_color_maps/cyclic/bamO.txt +256 -0
  32. bsplot/scientific_color_maps/cyclic/brocO.txt +256 -0
  33. bsplot/scientific_color_maps/cyclic/corkO.txt +256 -0
  34. bsplot/scientific_color_maps/cyclic/romaO.txt +256 -0
  35. bsplot/scientific_color_maps/cyclic/vikO.txt +256 -0
  36. bsplot/scientific_color_maps/diverging/bam.txt +256 -0
  37. bsplot/scientific_color_maps/diverging/berlin.txt +256 -0
  38. bsplot/scientific_color_maps/diverging/broc.txt +256 -0
  39. bsplot/scientific_color_maps/diverging/cork.txt +256 -0
  40. bsplot/scientific_color_maps/diverging/lisbon.txt +256 -0
  41. bsplot/scientific_color_maps/diverging/managua.txt +256 -0
  42. bsplot/scientific_color_maps/diverging/roma.txt +256 -0
  43. bsplot/scientific_color_maps/diverging/tofino.txt +256 -0
  44. bsplot/scientific_color_maps/diverging/vanimo.txt +256 -0
  45. bsplot/scientific_color_maps/diverging/vik.txt +256 -0
  46. bsplot/scientific_color_maps/multisequential/bukavu.txt +256 -0
  47. bsplot/scientific_color_maps/multisequential/fes.txt +256 -0
  48. bsplot/scientific_color_maps/multisequential/oleron.txt +256 -0
  49. bsplot/scientific_color_maps/naviaW.txt +256 -0
  50. bsplot/scientific_color_maps/sequential/acton.txt +256 -0
  51. bsplot/scientific_color_maps/sequential/bamako.txt +256 -0
  52. bsplot/scientific_color_maps/sequential/batlow.txt +256 -0
  53. bsplot/scientific_color_maps/sequential/bilbao.txt +256 -0
  54. bsplot/scientific_color_maps/sequential/buda.txt +256 -0
  55. bsplot/scientific_color_maps/sequential/davos.txt +256 -0
  56. bsplot/scientific_color_maps/sequential/devon.txt +256 -0
  57. bsplot/scientific_color_maps/sequential/glasgow.txt +256 -0
  58. bsplot/scientific_color_maps/sequential/grayC.txt +256 -0
  59. bsplot/scientific_color_maps/sequential/hawaii.txt +256 -0
  60. bsplot/scientific_color_maps/sequential/imola.txt +256 -0
  61. bsplot/scientific_color_maps/sequential/lajolla.txt +256 -0
  62. bsplot/scientific_color_maps/sequential/lapaz.txt +256 -0
  63. bsplot/scientific_color_maps/sequential/lipari.txt +256 -0
  64. bsplot/scientific_color_maps/sequential/navia.txt +256 -0
  65. bsplot/scientific_color_maps/sequential/nuuk.txt +256 -0
  66. bsplot/scientific_color_maps/sequential/oslo.txt +256 -0
  67. bsplot/scientific_color_maps/sequential/tokyo.txt +256 -0
  68. bsplot/scientific_color_maps/sequential/turku.txt +256 -0
  69. bsplot/streamlines.py +314 -0
  70. bsplot/style.py +791 -0
  71. bsplot/styles/black.mplstyle +78 -0
  72. bsplot/styles/bss.mplstyle +50 -0
  73. bsplot/styles/bwcomp.mplstyle +30 -0
  74. bsplot/styles/nature.mplstyle +57 -0
  75. bsplot/styles/transparent.mplstyle +16 -0
  76. bsplot/styles/tvbo.mplstyle +57 -0
  77. bsplot/surface.py +2370 -0
  78. bsplot/templates.py +270 -0
  79. bsplot/text.py +199 -0
  80. bsplot/text2obj.py +59 -0
  81. bsplot/timeseries.py +10 -0
  82. bsplot/utils.py +13 -0
  83. bsplot/volume.py +958 -0
  84. bsplot-0.0.2.dist-info/METADATA +60 -0
  85. bsplot-0.0.2.dist-info/RECORD +89 -0
  86. bsplot-0.0.2.dist-info/WHEEL +5 -0
  87. bsplot-0.0.2.dist-info/entry_points.txt +2 -0
  88. bsplot-0.0.2.dist-info/licenses/LICENSE +193 -0
  89. bsplot-0.0.2.dist-info/top_level.txt +1 -0
bsplot/bioicons.py ADDED
@@ -0,0 +1,777 @@
1
+ """
2
+ Bioicons Interface for matplotlib icon plotting.
3
+
4
+ This module provides an interface to fetch and plot scientific icons from
5
+ bioicons.com - a free, open-source library of scientific icons.
6
+
7
+ The module can be used to annotate any matplotlib axis with scientific icons,
8
+ making it ideal for creating publication-quality figures with consistent
9
+ scientific iconography.
10
+
11
+ Icons are organized by license/category/author on bioicons.com:
12
+ - Licenses: CC-0, CC-BY-3.0, CC-BY-4.0, CC-BY-SA, MIT, BSD
13
+ - Categories: Amino-Acids, Machine Learning, Animals, Blood Immunology,
14
+ Cell culture, Chemistry, Genetics, Lab apparatus, etc.
15
+
16
+ Usage:
17
+ ------
18
+ from bsplot.bioicons import BioIcons, plot_icon
19
+
20
+ # Initialize the icons interface
21
+ icons = BioIcons()
22
+
23
+ # Plot a single icon on any matplotlib axis
24
+ fig, ax = plt.subplots()
25
+ ax.plot([0, 1, 2], [1, 2, 1]) # Your plot
26
+ plot_icon("neuron", ax=ax, x=1, y=2, size=0.15) # Add icon annotation
27
+
28
+ # Or use the icons object for multiple icons
29
+ icons.plot("brain", ax=ax, x=0.5, y=1.5, size=0.1)
30
+
31
+ # Search for icons
32
+ icons.search("cell") # Find all icons with "cell" in the name
33
+ """
34
+
35
+ from __future__ import annotations
36
+
37
+ import io
38
+ import json
39
+ import hashlib
40
+ import warnings
41
+ from pathlib import Path
42
+ from typing import Optional, Union, Dict, Any, List
43
+ from urllib.request import urlopen, Request
44
+ from urllib.error import URLError, HTTPError
45
+
46
+ import matplotlib.pyplot as plt
47
+ import matplotlib.patches as mpatches
48
+ import matplotlib.image as mpimg
49
+ from matplotlib.offsetbox import OffsetImage, AnnotationBbox
50
+ from matplotlib.axes import Axes
51
+ from matplotlib.figure import Figure
52
+ import numpy as np
53
+
54
+ # Try to import optional dependencies
55
+ try:
56
+ from PIL import Image
57
+ HAS_PIL = True
58
+ except ImportError:
59
+ HAS_PIL = False
60
+
61
+ try:
62
+ import cairosvg
63
+ HAS_CAIROSVG = True
64
+ except ImportError:
65
+ HAS_CAIROSVG = False
66
+
67
+
68
+ # Default cache directory for downloaded icons
69
+ DEFAULT_CACHE_DIR = Path.home() / ".cache" / "bsplot" / "bioicons"
70
+
71
+ # Bioicons.com base URL for icons
72
+ BIOICONS_BASE_URL = "https://bioicons.com/icons"
73
+ BIOICONS_INDEX_URL = "https://bioicons.com/icons/icons.json"
74
+
75
+
76
+ class BioIcons:
77
+ """
78
+ Interface to fetch and manage bioicons for matplotlib plots.
79
+
80
+ This class provides methods to load, cache, and plot scientific icons
81
+ from bioicons.com on any matplotlib axis.
82
+
83
+ Parameters
84
+ ----------
85
+ cache_dir : str or Path, optional
86
+ Directory to cache downloaded icons. Defaults to ~/.cache/bsplot/bioicons
87
+ local_icons_dir : str or Path, optional
88
+ Directory containing local icon files (SVG or PNG) to use instead of fetching.
89
+
90
+ Examples
91
+ --------
92
+ >>> import matplotlib.pyplot as plt
93
+ >>> from bsplot.bioicons import BioIcons
94
+ >>>
95
+ >>> icons = BioIcons()
96
+ >>> fig, ax = plt.subplots()
97
+ >>> ax.plot([0, 1, 2], [1, 2, 1])
98
+ >>> icons.plot("neuron", ax=ax, x=1, y=2, size=0.15)
99
+ >>> plt.show()
100
+
101
+ >>> # Search for icons
102
+ >>> icons.search("cell")
103
+ >>>
104
+ >>> # Get icon by full path
105
+ >>> icons.plot("cc-0/Genetics/Simon Dürr/neuron", ax=ax, x=0, y=0, size=0.1)
106
+ """
107
+
108
+ def __init__(
109
+ self,
110
+ cache_dir: Optional[Union[str, Path]] = None,
111
+ local_icons_dir: Optional[Union[str, Path]] = None,
112
+ ):
113
+ self.cache_dir = Path(cache_dir) if cache_dir else DEFAULT_CACHE_DIR
114
+ self.local_icons_dir = Path(local_icons_dir) if local_icons_dir else None
115
+ self._icon_cache: Dict[str, np.ndarray] = {}
116
+ self._index: Optional[List[Dict[str, str]]] = None
117
+
118
+ # Create cache directory if it doesn't exist
119
+ self.cache_dir.mkdir(parents=True, exist_ok=True)
120
+
121
+ def _get_cache_path(self, icon_id: str, ext: str = "svg") -> Path:
122
+ """Get the cache file path for an icon."""
123
+ # Use hash to handle special characters in icon IDs
124
+ hash_id = hashlib.md5(icon_id.encode()).hexdigest()[:12]
125
+ safe_name = icon_id.replace("/", "_").replace(" ", "_")
126
+ return self.cache_dir / f"{hash_id}_{safe_name}.{ext}"
127
+
128
+ def _fetch_index(self) -> List[Dict[str, str]]:
129
+ """Fetch the icon index from bioicons.com."""
130
+ if self._index is not None:
131
+ return self._index
132
+
133
+ # Check for cached index
134
+ index_cache = self.cache_dir / "icons.json"
135
+ if index_cache.exists():
136
+ try:
137
+ with open(index_cache, "r") as f:
138
+ self._index = json.load(f)
139
+ return self._index
140
+ except (json.JSONDecodeError, IOError):
141
+ pass
142
+
143
+ # Fetch from remote
144
+ try:
145
+ req = Request(
146
+ BIOICONS_INDEX_URL,
147
+ headers={"User-Agent": "bsplot/1.0"}
148
+ )
149
+ with urlopen(req, timeout=10) as response:
150
+ data = response.read().decode("utf-8")
151
+ self._index = json.loads(data)
152
+
153
+ # Cache the index
154
+ with open(index_cache, "w") as f:
155
+ f.write(data)
156
+
157
+ return self._index
158
+ except (URLError, HTTPError) as e:
159
+ warnings.warn(f"Failed to fetch icon index: {e}")
160
+ return []
161
+ except Exception as e:
162
+ warnings.warn(f"Error processing icon index: {e}")
163
+ return []
164
+
165
+ def search(self, query: str, category: Optional[str] = None,
166
+ license: Optional[str] = None) -> List[Dict[str, str]]:
167
+ """
168
+ Search for icons matching the query.
169
+
170
+ Parameters
171
+ ----------
172
+ query : str
173
+ Search query (matches icon name, category, or author)
174
+ category : str, optional
175
+ Filter by category
176
+ license : str, optional
177
+ Filter by license type (e.g., "cc-0", "cc-by")
178
+
179
+ Returns
180
+ -------
181
+ list of dict
182
+ List of matching icons with keys: name, category, license, author
183
+ """
184
+ index = self._fetch_index()
185
+ query_lower = query.lower()
186
+
187
+ results = []
188
+ for icon in index:
189
+ # Match query against name, category, or author
190
+ if (query_lower in icon.get("name", "").lower() or
191
+ query_lower in icon.get("category", "").lower() or
192
+ query_lower in icon.get("author", "").lower()):
193
+
194
+ # Apply filters
195
+ if category and icon.get("category", "").lower() != category.lower():
196
+ continue
197
+ if license and license.lower() not in icon.get("license", "").lower():
198
+ continue
199
+
200
+ results.append(icon)
201
+
202
+ return results
203
+
204
+ def list_categories(self) -> List[str]:
205
+ """Get list of all available icon categories."""
206
+ index = self._fetch_index()
207
+ categories = set(icon.get("category", "") for icon in index)
208
+ return sorted(cat for cat in categories if cat)
209
+
210
+ def list_licenses(self) -> List[str]:
211
+ """Get list of all available licenses."""
212
+ index = self._fetch_index()
213
+ licenses = set(icon.get("license", "") for icon in index)
214
+ return sorted(lic for lic in licenses if lic)
215
+
216
+ def _build_icon_url(self, icon_info: Dict[str, str]) -> str:
217
+ """Build the URL for an icon from its info dict."""
218
+ license_dir = icon_info.get("license", "").replace(" ", "%20")
219
+ category = icon_info.get("category", "").replace(" ", "%20")
220
+ author = icon_info.get("author", "").replace(" ", "%20")
221
+ name = icon_info.get("name", "").replace(" ", "%20")
222
+
223
+ return f"{BIOICONS_BASE_URL}/{license_dir}/{category}/{author}/{name}.svg"
224
+
225
+ def _find_icon(self, icon_name: str) -> Optional[Dict[str, str]]:
226
+ """
227
+ Find an icon by name in the index.
228
+
229
+ If icon_name contains '/', treat it as a full path.
230
+ Otherwise, search by name and return the first match.
231
+ """
232
+ # Check if it's a full path
233
+ if "/" in icon_name:
234
+ parts = icon_name.split("/")
235
+ if len(parts) == 4:
236
+ return {
237
+ "license": parts[0],
238
+ "category": parts[1],
239
+ "author": parts[2],
240
+ "name": parts[3].replace(".svg", ""),
241
+ }
242
+
243
+ # Search by name
244
+ index = self._fetch_index()
245
+ for icon in index:
246
+ if icon.get("name", "").lower() == icon_name.lower():
247
+ return icon
248
+ # Also match without extension
249
+ if icon.get("name", "").lower().replace(".svg", "") == icon_name.lower():
250
+ return icon
251
+
252
+ # Partial match
253
+ for icon in index:
254
+ if icon_name.lower() in icon.get("name", "").lower():
255
+ return icon
256
+
257
+ return None
258
+
259
+ def _load_from_local(self, icon_name: str) -> Optional[np.ndarray]:
260
+ """Try to load icon from local icons directory."""
261
+ if self.local_icons_dir is None:
262
+ return None
263
+
264
+ # Try different file extensions
265
+ for ext in ["svg", "png", "jpg", "jpeg"]:
266
+ icon_path = self.local_icons_dir / f"{icon_name}.{ext}"
267
+ if icon_path.exists():
268
+ return self._load_image_file(icon_path)
269
+ return None
270
+
271
+ def _load_from_cache(self, icon_id: str) -> Optional[np.ndarray]:
272
+ """Try to load icon from cache."""
273
+ for ext in ["png", "svg"]:
274
+ cache_path = self._get_cache_path(icon_id, ext)
275
+ if cache_path.exists():
276
+ return self._load_image_file(cache_path)
277
+ return None
278
+
279
+ def _load_image_file(self, path: Path) -> np.ndarray:
280
+ """Load an image file and return as numpy array."""
281
+ if path.suffix.lower() == ".svg":
282
+ return self._load_svg(path)
283
+ else:
284
+ if HAS_PIL:
285
+ img = Image.open(path).convert("RGBA")
286
+ return np.array(img)
287
+ else:
288
+ return mpimg.imread(str(path))
289
+
290
+ def _load_svg(self, path: Path) -> np.ndarray:
291
+ """Load SVG file and convert to numpy array."""
292
+ if not HAS_CAIROSVG:
293
+ raise ImportError(
294
+ "cairosvg is required to load SVG files. "
295
+ "Install with: pip install cairosvg"
296
+ )
297
+ if not HAS_PIL:
298
+ raise ImportError(
299
+ "PIL is required to process images. "
300
+ "Install with: pip install pillow"
301
+ )
302
+
303
+ # Convert SVG to PNG in memory
304
+ # unsafe=True allows SVGs with external entities (some bioicons use these)
305
+ png_data = cairosvg.svg2png(
306
+ url=str(path), output_width=256, output_height=256, unsafe=True
307
+ )
308
+ img = Image.open(io.BytesIO(png_data)).convert("RGBA")
309
+ return np.array(img)
310
+
311
+ def _load_svg_from_bytes(self, svg_data: bytes) -> np.ndarray:
312
+ """Load SVG from bytes and convert to numpy array."""
313
+ if not HAS_CAIROSVG:
314
+ raise ImportError(
315
+ "cairosvg is required to load SVG files. "
316
+ "Install with: pip install cairosvg"
317
+ )
318
+ if not HAS_PIL:
319
+ raise ImportError(
320
+ "PIL is required to process images. "
321
+ "Install with: pip install pillow"
322
+ )
323
+
324
+ # Convert SVG to PNG in memory
325
+ # unsafe=True allows SVGs with external entities (some bioicons use these)
326
+ png_data = cairosvg.svg2png(
327
+ bytestring=svg_data, output_width=256, output_height=256, unsafe=True
328
+ )
329
+ img = Image.open(io.BytesIO(png_data)).convert("RGBA")
330
+ return np.array(img)
331
+
332
+ def _fetch_icon(self, icon_name: str) -> Optional[np.ndarray]:
333
+ """
334
+ Fetch icon from bioicons.com.
335
+ """
336
+ # Check in-memory cache first
337
+ if icon_name in self._icon_cache:
338
+ return self._icon_cache[icon_name]
339
+
340
+ # Check local directory
341
+ img = self._load_from_local(icon_name)
342
+ if img is not None:
343
+ self._icon_cache[icon_name] = img
344
+ return img
345
+
346
+ # Check file cache
347
+ img = self._load_from_cache(icon_name)
348
+ if img is not None:
349
+ self._icon_cache[icon_name] = img
350
+ return img
351
+
352
+ # Find icon in index
353
+ icon_info = self._find_icon(icon_name)
354
+ if icon_info is None:
355
+ warnings.warn(
356
+ f"Icon '{icon_name}' not found. Use search() to find available icons."
357
+ )
358
+ return None
359
+
360
+ # Build URL and fetch
361
+ url = self._build_icon_url(icon_info)
362
+
363
+ try:
364
+ req = Request(url, headers={"User-Agent": "bsplot/1.0"})
365
+ with urlopen(req, timeout=10) as response:
366
+ if response.status == 200:
367
+ svg_data = response.read()
368
+
369
+ # Cache the SVG
370
+ cache_path = self._get_cache_path(icon_name, "svg")
371
+ with open(cache_path, "wb") as f:
372
+ f.write(svg_data)
373
+
374
+ # Convert to numpy array
375
+ img = self._load_svg_from_bytes(svg_data)
376
+ self._icon_cache[icon_name] = img
377
+ return img
378
+
379
+ except (URLError, HTTPError) as e:
380
+ warnings.warn(f"Failed to fetch icon '{icon_name}': {e}")
381
+ except ImportError as e:
382
+ warnings.warn(str(e))
383
+ except Exception as e:
384
+ warnings.warn(f"Error processing icon '{icon_name}': {e}")
385
+
386
+ return None
387
+
388
+ def get_icon(self, icon_name: str) -> Optional[np.ndarray]:
389
+ """
390
+ Get an icon as a numpy array.
391
+
392
+ Parameters
393
+ ----------
394
+ icon_name : str
395
+ Name of the icon to fetch. Can be:
396
+ - A simple name like "neuron" (will search and use first match)
397
+ - A full path like "cc-0/Genetics/Simon Dürr/neuron"
398
+
399
+ Returns
400
+ -------
401
+ np.ndarray or None
402
+ Icon as RGBA numpy array, or None if not found
403
+ """
404
+ return self._fetch_icon(icon_name)
405
+
406
+ def _create_placeholder(
407
+ self,
408
+ icon_name: str,
409
+ size: int = 64,
410
+ color: str = "#4a90e2",
411
+ ) -> np.ndarray:
412
+ """Create a placeholder icon when the actual icon is not available."""
413
+ if not HAS_PIL:
414
+ # Return a simple colored square
415
+ arr = np.ones((size, size, 4), dtype=np.float32)
416
+ if color.startswith("#"):
417
+ r = int(color[1:3], 16) / 255
418
+ g = int(color[3:5], 16) / 255
419
+ b = int(color[5:7], 16) / 255
420
+ else:
421
+ r, g, b = 0.3, 0.6, 0.9
422
+ arr[:, :, 0] = r
423
+ arr[:, :, 1] = g
424
+ arr[:, :, 2] = b
425
+ arr[:, :, 3] = 0.8
426
+ return arr
427
+
428
+ from PIL import Image, ImageDraw
429
+
430
+ img = Image.new("RGBA", (size, size), (255, 255, 255, 0))
431
+ draw = ImageDraw.Draw(img)
432
+
433
+ # Parse color
434
+ if color.startswith("#"):
435
+ r = int(color[1:3], 16)
436
+ g = int(color[3:5], 16)
437
+ b = int(color[5:7], 16)
438
+ fill_color = (r, g, b, 200)
439
+ else:
440
+ fill_color = (74, 144, 226, 200)
441
+
442
+ # Draw a rounded rectangle
443
+ margin = size // 8
444
+ draw.rounded_rectangle(
445
+ [margin, margin, size - margin, size - margin],
446
+ radius=size // 6,
447
+ fill=fill_color,
448
+ )
449
+
450
+ # Add first letter of icon name
451
+ letter = icon_name[0].upper() if icon_name else "?"
452
+ bbox = draw.textbbox((0, 0), letter)
453
+ text_w = bbox[2] - bbox[0]
454
+ text_h = bbox[3] - bbox[1]
455
+ text_x = (size - text_w) // 2
456
+ text_y = (size - text_h) // 2 - 2
457
+ draw.text((text_x, text_y), letter, fill=(255, 255, 255, 255))
458
+
459
+ return np.array(img)
460
+
461
+ def plot(
462
+ self,
463
+ icon_name: str,
464
+ ax: Optional[Axes] = None,
465
+ x: float = 0.5,
466
+ y: float = 0.5,
467
+ size: float = 0.1,
468
+ zoom: float = 1.0,
469
+ alpha: float = 1.0,
470
+ zorder: int = 10,
471
+ frameon: bool = False,
472
+ box_alignment: tuple = (0.5, 0.5),
473
+ xycoords: str = "data",
474
+ **kwargs,
475
+ ) -> Optional[AnnotationBbox]:
476
+ """
477
+ Plot an icon on a matplotlib axis.
478
+
479
+ Parameters
480
+ ----------
481
+ icon_name : str
482
+ Name or path of the icon to plot
483
+ ax : matplotlib.axes.Axes, optional
484
+ Axis to plot on. If None, uses current axis.
485
+ x : float
486
+ X coordinate for icon placement
487
+ y : float
488
+ Y coordinate for icon placement
489
+ size : float
490
+ Size of the icon relative to the figure (default 0.1)
491
+ zoom : float
492
+ Additional zoom factor (default 1.0)
493
+ alpha : float
494
+ Transparency (0-1, default 1.0)
495
+ zorder : int
496
+ Z-order for layering (default 10)
497
+ frameon : bool
498
+ Whether to show a frame around the icon (default False)
499
+ box_alignment : tuple
500
+ Alignment of the icon box (default (0.5, 0.5) for center)
501
+ xycoords : str
502
+ Coordinate system for x, y (default "data")
503
+ **kwargs
504
+ Additional arguments passed to AnnotationBbox
505
+
506
+ Returns
507
+ -------
508
+ AnnotationBbox or None
509
+ The annotation box artist, or None if icon not found
510
+ """
511
+ if ax is None:
512
+ ax = plt.gca()
513
+
514
+ # Get icon image
515
+ img = self._fetch_icon(icon_name)
516
+ if img is None:
517
+ # Use placeholder
518
+ warnings.warn(f"Using placeholder for '{icon_name}'")
519
+ img = self._create_placeholder(icon_name)
520
+
521
+ # Calculate zoom based on desired size
522
+ fig = ax.get_figure()
523
+ fig_width, fig_height = fig.get_size_inches()
524
+ dpi = fig.dpi
525
+
526
+ # Target size in pixels
527
+ target_size = size * min(fig_width, fig_height) * dpi
528
+ current_size = max(img.shape[:2])
529
+ calculated_zoom = (target_size / current_size) * zoom
530
+
531
+ # Create offset image
532
+ imagebox = OffsetImage(img, zoom=calculated_zoom, alpha=alpha)
533
+
534
+ # Create annotation box
535
+ ab = AnnotationBbox(
536
+ imagebox,
537
+ (x, y),
538
+ xycoords=xycoords,
539
+ frameon=frameon,
540
+ box_alignment=box_alignment,
541
+ zorder=zorder,
542
+ **kwargs,
543
+ )
544
+
545
+ ax.add_artist(ab)
546
+ return ab
547
+
548
+ def plot_grid(
549
+ self,
550
+ icon_names: List[str],
551
+ ncols: int = 4,
552
+ figsize: Optional[tuple] = None,
553
+ show_labels: bool = True,
554
+ title: Optional[str] = None,
555
+ ) -> Figure:
556
+ """
557
+ Plot a grid of icons.
558
+
559
+ Parameters
560
+ ----------
561
+ icon_names : list of str
562
+ Names of icons to plot
563
+ ncols : int
564
+ Number of columns in the grid
565
+ figsize : tuple, optional
566
+ Figure size (width, height) in inches
567
+ show_labels : bool
568
+ Whether to show icon names below each icon
569
+ title : str, optional
570
+ Title for the figure
571
+
572
+ Returns
573
+ -------
574
+ matplotlib.figure.Figure
575
+ The figure containing the icon grid
576
+ """
577
+ n_icons = len(icon_names)
578
+ nrows = (n_icons + ncols - 1) // ncols
579
+
580
+ if figsize is None:
581
+ figsize = (ncols * 2, nrows * (2.5 if show_labels else 2))
582
+
583
+ fig, axes = plt.subplots(nrows, ncols, figsize=figsize)
584
+ if nrows == 1 and ncols == 1:
585
+ axes = np.array([[axes]])
586
+ elif nrows == 1:
587
+ axes = axes.reshape(1, -1)
588
+ elif ncols == 1:
589
+ axes = axes.reshape(-1, 1)
590
+
591
+ for idx, name in enumerate(icon_names):
592
+ row = idx // ncols
593
+ col = idx % ncols
594
+ ax = axes[row, col]
595
+
596
+ img = self._fetch_icon(name)
597
+ if img is None:
598
+ img = self._create_placeholder(name)
599
+
600
+ ax.imshow(img)
601
+ ax.axis("off")
602
+
603
+ if show_labels:
604
+ ax.set_title(name, fontsize=9, wrap=True)
605
+
606
+ # Hide empty subplots
607
+ for idx in range(n_icons, nrows * ncols):
608
+ row = idx // ncols
609
+ col = idx % ncols
610
+ axes[row, col].axis("off")
611
+
612
+ if title:
613
+ fig.suptitle(title, fontsize=14)
614
+
615
+ plt.tight_layout()
616
+ return fig
617
+
618
+
619
+ class IconAnnotation:
620
+ """
621
+ Helper class for annotating matplotlib plots with icons.
622
+
623
+ This class provides a simpler interface for adding icons as annotations
624
+ to existing matplotlib figures.
625
+
626
+ Parameters
627
+ ----------
628
+ icons : BioIcons, optional
629
+ BioIcons instance to use. If None, creates a new one.
630
+ """
631
+
632
+ def __init__(self, icons: Optional[BioIcons] = None):
633
+ self.icons = icons or BioIcons()
634
+
635
+ def annotate(
636
+ self,
637
+ ax: Axes,
638
+ icon_name: str,
639
+ xy: tuple,
640
+ size: float = 0.1,
641
+ **kwargs,
642
+ ) -> Optional[AnnotationBbox]:
643
+ """
644
+ Add an icon annotation to an axis.
645
+
646
+ Parameters
647
+ ----------
648
+ ax : matplotlib.axes.Axes
649
+ Axis to annotate
650
+ icon_name : str
651
+ Name of the icon
652
+ xy : tuple
653
+ (x, y) position for the icon
654
+ size : float
655
+ Size of the icon
656
+ **kwargs
657
+ Additional arguments passed to BioIcons.plot()
658
+ """
659
+ return self.icons.plot(icon_name, ax=ax, x=xy[0], y=xy[1], size=size, **kwargs)
660
+
661
+
662
+ # Module-level convenience functions
663
+
664
+ _default_icons: Optional[BioIcons] = None
665
+
666
+
667
+ def _get_default_icons() -> BioIcons:
668
+ """Get or create the default BioIcons instance."""
669
+ global _default_icons
670
+ if _default_icons is None:
671
+ _default_icons = BioIcons()
672
+ return _default_icons
673
+
674
+
675
+ def plot_icon(
676
+ icon_name: str,
677
+ ax: Optional[Axes] = None,
678
+ x: float = 0.5,
679
+ y: float = 0.5,
680
+ size: float = 0.1,
681
+ **kwargs,
682
+ ) -> Optional[AnnotationBbox]:
683
+ """
684
+ Plot an icon on a matplotlib axis.
685
+
686
+ This is a convenience function that uses the default BioIcons instance.
687
+
688
+ Parameters
689
+ ----------
690
+ icon_name : str
691
+ Name or path of the icon to plot
692
+ ax : matplotlib.axes.Axes, optional
693
+ Axis to plot on. If None, uses current axis.
694
+ x : float
695
+ X coordinate for icon placement
696
+ y : float
697
+ Y coordinate for icon placement
698
+ size : float
699
+ Size of the icon relative to the figure (default 0.1)
700
+ **kwargs
701
+ Additional arguments passed to BioIcons.plot()
702
+
703
+ Returns
704
+ -------
705
+ AnnotationBbox or None
706
+ The annotation box artist, or None if icon not found
707
+
708
+ Examples
709
+ --------
710
+ >>> import matplotlib.pyplot as plt
711
+ >>> from bsplot.bioicons import plot_icon
712
+ >>>
713
+ >>> fig, ax = plt.subplots()
714
+ >>> ax.plot([0, 1, 2], [0, 1, 0])
715
+ >>> plot_icon("neuron", ax=ax, x=1, y=1, size=0.15)
716
+ >>> plt.show()
717
+ """
718
+ icons = _get_default_icons()
719
+ return icons.plot(icon_name, ax=ax, x=x, y=y, size=size, **kwargs)
720
+
721
+
722
+ def plot_icons_grid(
723
+ icon_names: List[str],
724
+ ncols: int = 4,
725
+ **kwargs,
726
+ ) -> Figure:
727
+ """
728
+ Plot a grid of icons.
729
+
730
+ Parameters
731
+ ----------
732
+ icon_names : list of str
733
+ Names of icons to plot
734
+ ncols : int
735
+ Number of columns in the grid
736
+ **kwargs
737
+ Additional arguments passed to BioIcons.plot_grid()
738
+
739
+ Returns
740
+ -------
741
+ matplotlib.figure.Figure
742
+ The figure containing the icon grid
743
+ """
744
+ icons = _get_default_icons()
745
+ return icons.plot_grid(icon_names, ncols=ncols, **kwargs)
746
+
747
+
748
+ def search_icons(query: str, **kwargs) -> List[Dict[str, str]]:
749
+ """
750
+ Search for icons matching the query.
751
+
752
+ Parameters
753
+ ----------
754
+ query : str
755
+ Search query
756
+ **kwargs
757
+ Additional arguments passed to BioIcons.search()
758
+
759
+ Returns
760
+ -------
761
+ list of dict
762
+ Matching icons
763
+ """
764
+ icons = _get_default_icons()
765
+ return icons.search(query, **kwargs)
766
+
767
+
768
+ def list_categories() -> List[str]:
769
+ """Get list of all available icon categories."""
770
+ icons = _get_default_icons()
771
+ return icons.list_categories()
772
+
773
+
774
+ def list_licenses() -> List[str]:
775
+ """Get list of all available licenses."""
776
+ icons = _get_default_icons()
777
+ return icons.list_licenses()