scitex 2.14.0__py3-none-any.whl → 2.15.1__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 (218) hide show
  1. scitex/__init__.py +47 -0
  2. scitex/_env_loader.py +156 -0
  3. scitex/_mcp_resources/__init__.py +37 -0
  4. scitex/_mcp_resources/_cheatsheet.py +135 -0
  5. scitex/_mcp_resources/_figrecipe.py +138 -0
  6. scitex/_mcp_resources/_formats.py +102 -0
  7. scitex/_mcp_resources/_modules.py +337 -0
  8. scitex/_mcp_resources/_session.py +149 -0
  9. scitex/_mcp_tools/__init__.py +4 -0
  10. scitex/_mcp_tools/audio.py +66 -0
  11. scitex/_mcp_tools/diagram.py +11 -95
  12. scitex/_mcp_tools/introspect.py +191 -0
  13. scitex/_mcp_tools/plt.py +260 -305
  14. scitex/_mcp_tools/scholar.py +74 -0
  15. scitex/_mcp_tools/social.py +244 -0
  16. scitex/_mcp_tools/writer.py +21 -204
  17. scitex/ai/_gen_ai/_PARAMS.py +10 -7
  18. scitex/ai/classification/reporters/_SingleClassificationReporter.py +45 -1603
  19. scitex/ai/classification/reporters/_mixins/__init__.py +36 -0
  20. scitex/ai/classification/reporters/_mixins/_constants.py +67 -0
  21. scitex/ai/classification/reporters/_mixins/_cv_summary.py +387 -0
  22. scitex/ai/classification/reporters/_mixins/_feature_importance.py +119 -0
  23. scitex/ai/classification/reporters/_mixins/_metrics.py +275 -0
  24. scitex/ai/classification/reporters/_mixins/_plotting.py +179 -0
  25. scitex/ai/classification/reporters/_mixins/_reports.py +153 -0
  26. scitex/ai/classification/reporters/_mixins/_storage.py +160 -0
  27. scitex/audio/README.md +40 -36
  28. scitex/audio/__init__.py +127 -59
  29. scitex/audio/_branding.py +185 -0
  30. scitex/audio/_mcp/__init__.py +32 -0
  31. scitex/audio/_mcp/handlers.py +59 -6
  32. scitex/audio/_mcp/speak_handlers.py +238 -0
  33. scitex/audio/_relay.py +225 -0
  34. scitex/audio/engines/elevenlabs_engine.py +6 -1
  35. scitex/audio/mcp_server.py +228 -75
  36. scitex/canvas/README.md +1 -1
  37. scitex/canvas/editor/_dearpygui/__init__.py +25 -0
  38. scitex/canvas/editor/_dearpygui/_editor.py +147 -0
  39. scitex/canvas/editor/_dearpygui/_handlers.py +476 -0
  40. scitex/canvas/editor/_dearpygui/_panels/__init__.py +17 -0
  41. scitex/canvas/editor/_dearpygui/_panels/_control.py +119 -0
  42. scitex/canvas/editor/_dearpygui/_panels/_element_controls.py +190 -0
  43. scitex/canvas/editor/_dearpygui/_panels/_preview.py +43 -0
  44. scitex/canvas/editor/_dearpygui/_panels/_sections.py +390 -0
  45. scitex/canvas/editor/_dearpygui/_plotting.py +187 -0
  46. scitex/canvas/editor/_dearpygui/_rendering.py +504 -0
  47. scitex/canvas/editor/_dearpygui/_selection.py +295 -0
  48. scitex/canvas/editor/_dearpygui/_state.py +93 -0
  49. scitex/canvas/editor/_dearpygui/_utils.py +61 -0
  50. scitex/canvas/editor/flask_editor/templates/__init__.py +32 -70
  51. scitex/cli/__init__.py +38 -43
  52. scitex/cli/audio.py +76 -27
  53. scitex/cli/capture.py +13 -20
  54. scitex/cli/introspect.py +443 -0
  55. scitex/cli/main.py +198 -109
  56. scitex/cli/mcp.py +60 -34
  57. scitex/cli/scholar/__init__.py +8 -0
  58. scitex/cli/scholar/_crossref_scitex.py +296 -0
  59. scitex/cli/scholar/_fetch.py +25 -3
  60. scitex/cli/social.py +314 -0
  61. scitex/cli/writer.py +117 -0
  62. scitex/config/README.md +1 -1
  63. scitex/config/__init__.py +16 -2
  64. scitex/config/_env_registry.py +191 -0
  65. scitex/diagram/__init__.py +42 -19
  66. scitex/diagram/mcp_server.py +13 -125
  67. scitex/introspect/__init__.py +75 -0
  68. scitex/introspect/_call_graph.py +303 -0
  69. scitex/introspect/_class_hierarchy.py +163 -0
  70. scitex/introspect/_core.py +42 -0
  71. scitex/introspect/_docstring.py +131 -0
  72. scitex/introspect/_examples.py +113 -0
  73. scitex/introspect/_imports.py +271 -0
  74. scitex/introspect/_mcp/__init__.py +37 -0
  75. scitex/introspect/_mcp/handlers.py +208 -0
  76. scitex/introspect/_members.py +151 -0
  77. scitex/introspect/_resolve.py +89 -0
  78. scitex/introspect/_signature.py +131 -0
  79. scitex/introspect/_source.py +80 -0
  80. scitex/introspect/_type_hints.py +172 -0
  81. scitex/io/bundle/README.md +1 -1
  82. scitex/mcp_server.py +98 -5
  83. scitex/plt/__init__.py +248 -550
  84. scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin/_wrappers.py +5 -10
  85. scitex/plt/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
  86. scitex/plt/gallery/README.md +1 -1
  87. scitex/plt/utils/_hitmap/__init__.py +82 -0
  88. scitex/plt/utils/_hitmap/_artist_extraction.py +343 -0
  89. scitex/plt/utils/_hitmap/_color_application.py +346 -0
  90. scitex/plt/utils/_hitmap/_color_conversion.py +121 -0
  91. scitex/plt/utils/_hitmap/_constants.py +40 -0
  92. scitex/plt/utils/_hitmap/_hitmap_core.py +334 -0
  93. scitex/plt/utils/_hitmap/_path_extraction.py +357 -0
  94. scitex/plt/utils/_hitmap/_query.py +113 -0
  95. scitex/plt/utils/_hitmap.py +46 -1616
  96. scitex/plt/utils/_metadata/__init__.py +80 -0
  97. scitex/plt/utils/_metadata/_artists/__init__.py +25 -0
  98. scitex/plt/utils/_metadata/_artists/_base.py +195 -0
  99. scitex/plt/utils/_metadata/_artists/_collections.py +356 -0
  100. scitex/plt/utils/_metadata/_artists/_extract.py +57 -0
  101. scitex/plt/utils/_metadata/_artists/_images.py +80 -0
  102. scitex/plt/utils/_metadata/_artists/_lines.py +261 -0
  103. scitex/plt/utils/_metadata/_artists/_patches.py +247 -0
  104. scitex/plt/utils/_metadata/_artists/_text.py +106 -0
  105. scitex/plt/utils/_metadata/_csv.py +416 -0
  106. scitex/plt/utils/_metadata/_detect.py +225 -0
  107. scitex/plt/utils/_metadata/_legend.py +127 -0
  108. scitex/plt/utils/_metadata/_rounding.py +117 -0
  109. scitex/plt/utils/_metadata/_verification.py +202 -0
  110. scitex/schema/README.md +1 -1
  111. scitex/scholar/__init__.py +8 -0
  112. scitex/scholar/_mcp/crossref_handlers.py +265 -0
  113. scitex/scholar/core/Scholar.py +63 -1700
  114. scitex/scholar/core/_mixins/__init__.py +36 -0
  115. scitex/scholar/core/_mixins/_enrichers.py +270 -0
  116. scitex/scholar/core/_mixins/_library_handlers.py +100 -0
  117. scitex/scholar/core/_mixins/_loaders.py +103 -0
  118. scitex/scholar/core/_mixins/_pdf_download.py +375 -0
  119. scitex/scholar/core/_mixins/_pipeline.py +312 -0
  120. scitex/scholar/core/_mixins/_project_handlers.py +125 -0
  121. scitex/scholar/core/_mixins/_savers.py +69 -0
  122. scitex/scholar/core/_mixins/_search.py +103 -0
  123. scitex/scholar/core/_mixins/_services.py +88 -0
  124. scitex/scholar/core/_mixins/_url_finding.py +105 -0
  125. scitex/scholar/crossref_scitex.py +367 -0
  126. scitex/scholar/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
  127. scitex/scholar/examples/00_run_all.sh +120 -0
  128. scitex/scholar/jobs/_executors.py +27 -3
  129. scitex/scholar/pdf_download/ScholarPDFDownloader.py +38 -416
  130. scitex/scholar/pdf_download/_cli.py +154 -0
  131. scitex/scholar/pdf_download/strategies/__init__.py +11 -8
  132. scitex/scholar/pdf_download/strategies/manual_download_fallback.py +80 -3
  133. scitex/scholar/pipelines/ScholarPipelineBibTeX.py +73 -121
  134. scitex/scholar/pipelines/ScholarPipelineParallel.py +80 -138
  135. scitex/scholar/pipelines/ScholarPipelineSingle.py +43 -63
  136. scitex/scholar/pipelines/_single_steps.py +71 -36
  137. scitex/scholar/storage/_LibraryManager.py +97 -1695
  138. scitex/scholar/storage/_mixins/__init__.py +30 -0
  139. scitex/scholar/storage/_mixins/_bibtex_handlers.py +128 -0
  140. scitex/scholar/storage/_mixins/_library_operations.py +218 -0
  141. scitex/scholar/storage/_mixins/_metadata_conversion.py +226 -0
  142. scitex/scholar/storage/_mixins/_paper_saving.py +456 -0
  143. scitex/scholar/storage/_mixins/_resolution.py +376 -0
  144. scitex/scholar/storage/_mixins/_storage_helpers.py +121 -0
  145. scitex/scholar/storage/_mixins/_symlink_handlers.py +226 -0
  146. scitex/scholar/url_finder/.tmp/open_url/KNOWN_RESOLVERS.py +462 -0
  147. scitex/scholar/url_finder/.tmp/open_url/README.md +223 -0
  148. scitex/scholar/url_finder/.tmp/open_url/_DOIToURLResolver.py +694 -0
  149. scitex/scholar/url_finder/.tmp/open_url/_OpenURLResolver.py +1160 -0
  150. scitex/scholar/url_finder/.tmp/open_url/_ResolverLinkFinder.py +344 -0
  151. scitex/scholar/url_finder/.tmp/open_url/__init__.py +24 -0
  152. scitex/security/README.md +3 -3
  153. scitex/session/README.md +1 -1
  154. scitex/sh/README.md +1 -1
  155. scitex/social/__init__.py +153 -0
  156. scitex/social/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
  157. scitex/template/README.md +1 -1
  158. scitex/template/clone_writer_directory.py +5 -5
  159. scitex/writer/README.md +1 -1
  160. scitex/writer/_mcp/handlers.py +11 -744
  161. scitex/writer/_mcp/tool_schemas.py +5 -335
  162. scitex-2.15.1.dist-info/METADATA +648 -0
  163. {scitex-2.14.0.dist-info → scitex-2.15.1.dist-info}/RECORD +166 -111
  164. scitex/canvas/editor/flask_editor/templates/_scripts.py +0 -4933
  165. scitex/canvas/editor/flask_editor/templates/_styles.py +0 -1658
  166. scitex/dev/plt/data/mpl/PLOTTING_FUNCTIONS.yaml +0 -90
  167. scitex/dev/plt/data/mpl/PLOTTING_SIGNATURES.yaml +0 -1571
  168. scitex/dev/plt/data/mpl/PLOTTING_SIGNATURES_DETAILED.yaml +0 -6262
  169. scitex/dev/plt/data/mpl/SIGNATURES_FLATTENED.yaml +0 -1274
  170. scitex/dev/plt/data/mpl/dir_ax.txt +0 -459
  171. scitex/diagram/_compile.py +0 -312
  172. scitex/diagram/_diagram.py +0 -355
  173. scitex/diagram/_mcp/__init__.py +0 -4
  174. scitex/diagram/_mcp/handlers.py +0 -400
  175. scitex/diagram/_mcp/tool_schemas.py +0 -157
  176. scitex/diagram/_presets.py +0 -173
  177. scitex/diagram/_schema.py +0 -182
  178. scitex/diagram/_split.py +0 -278
  179. scitex/plt/_mcp/__init__.py +0 -4
  180. scitex/plt/_mcp/_handlers_annotation.py +0 -102
  181. scitex/plt/_mcp/_handlers_figure.py +0 -195
  182. scitex/plt/_mcp/_handlers_plot.py +0 -252
  183. scitex/plt/_mcp/_handlers_style.py +0 -219
  184. scitex/plt/_mcp/handlers.py +0 -74
  185. scitex/plt/_mcp/tool_schemas.py +0 -497
  186. scitex/plt/mcp_server.py +0 -231
  187. scitex/scholar/data/.gitkeep +0 -0
  188. scitex/scholar/data/README.md +0 -44
  189. scitex/scholar/data/bib_files/bibliography.bib +0 -1952
  190. scitex/scholar/data/bib_files/neurovista.bib +0 -277
  191. scitex/scholar/data/bib_files/neurovista_enriched.bib +0 -441
  192. scitex/scholar/data/bib_files/neurovista_enriched_enriched.bib +0 -441
  193. scitex/scholar/data/bib_files/neurovista_processed.bib +0 -338
  194. scitex/scholar/data/bib_files/openaccess.bib +0 -89
  195. scitex/scholar/data/bib_files/pac-seizure_prediction_enriched.bib +0 -2178
  196. scitex/scholar/data/bib_files/pac.bib +0 -698
  197. scitex/scholar/data/bib_files/pac_enriched.bib +0 -1061
  198. scitex/scholar/data/bib_files/pac_processed.bib +0 -0
  199. scitex/scholar/data/bib_files/pac_titles.txt +0 -75
  200. scitex/scholar/data/bib_files/paywalled.bib +0 -98
  201. scitex/scholar/data/bib_files/related-papers-by-coauthors.bib +0 -58
  202. scitex/scholar/data/bib_files/related-papers-by-coauthors_enriched.bib +0 -87
  203. scitex/scholar/data/bib_files/seizure_prediction.bib +0 -694
  204. scitex/scholar/data/bib_files/seizure_prediction_processed.bib +0 -0
  205. scitex/scholar/data/bib_files/test_complete_enriched.bib +0 -437
  206. scitex/scholar/data/bib_files/test_final_enriched.bib +0 -437
  207. scitex/scholar/data/bib_files/test_seizure.bib +0 -46
  208. scitex/scholar/data/impact_factor/JCR_IF_2022.xlsx +0 -0
  209. scitex/scholar/data/impact_factor/JCR_IF_2024.db +0 -0
  210. scitex/scholar/data/impact_factor/JCR_IF_2024.xlsx +0 -0
  211. scitex/scholar/data/impact_factor/JCR_IF_2024_v01.db +0 -0
  212. scitex/scholar/data/impact_factor.db +0 -0
  213. scitex/scholar/examples/SUGGESTIONS.md +0 -865
  214. scitex/scholar/examples/dev.py +0 -38
  215. scitex-2.14.0.dist-info/METADATA +0 -1238
  216. {scitex-2.14.0.dist-info → scitex-2.15.1.dist-info}/WHEEL +0 -0
  217. {scitex-2.14.0.dist-info → scitex-2.15.1.dist-info}/entry_points.txt +0 -0
  218. {scitex-2.14.0.dist-info → scitex-2.15.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
2
  # Timestamp: "2025-12-13 (ywatanabe)"
4
3
  # File: _wrappers.py - Seaborn plot wrappers
5
4
 
@@ -113,20 +112,16 @@ class SeabornWrappersMixin:
113
112
  "sns_boxplot", data=data, x=x, y=y, track=track, id=id, **kwargs
114
113
  )
115
114
 
116
- # Post-processing: Style boxplot with black medians
115
+ # Post-processing: Style boxplot elements (0.2mm black lines)
117
116
  from scitex.plt.utils import mm_to_pt
118
117
 
119
118
  lw_pt = mm_to_pt(0.2)
120
-
121
119
  for line in self._axis_mpl.get_lines():
122
120
  line.set_linewidth(lw_pt)
123
- xdata = line.get_xdata()
124
- ydata = line.get_ydata()
125
- if len(xdata) == 2 and len(ydata) == 2:
126
- if ydata[0] == ydata[1]:
127
- x_span = abs(xdata[1] - xdata[0])
128
- if x_span < 0.4:
129
- line.set_color("black")
121
+ line.set_color("black")
122
+ for patch in self._axis_mpl.patches:
123
+ patch.set_linewidth(lw_pt)
124
+ patch.set_edgecolor("black")
130
125
 
131
126
  if strip:
132
127
  strip_kwargs = kwargs.copy()
@@ -0,0 +1,149 @@
1
+ # External Package Branding Guide
2
+
3
+ When scitex wraps external packages (like `figrecipe` for `scitex.plt` or `crossref-local` for `scitex.scholar`), those packages should support configurable branding so documentation and error messages show the scitex namespace.
4
+
5
+ ## When to Use Branding
6
+
7
+ - **Use branding**: External packages that scitex wraps (figrecipe, crossref-local, etc.)
8
+ - **Don't use branding**: Internal scitex modules (scitex.audio, scitex.stats, etc.) - just hardcode `SCITEX_*` prefix
9
+
10
+ ## Pattern Overview
11
+
12
+ The external package provides a `_branding.py` module that:
13
+ 1. Reads brand name from environment variable
14
+ 2. Derives environment variable prefix from brand name
15
+ 3. Provides helper functions for rebranding text/docstrings
16
+
17
+ ## Implementation Template
18
+
19
+ ```python
20
+ # external_package/_branding.py
21
+
22
+ import os
23
+ import re
24
+ from typing import Optional
25
+
26
+ # Environment variables for branding
27
+ # Parent package sets these before importing
28
+ BRAND_NAME = os.environ.get("{PACKAGE}_BRAND", "{package}")
29
+ BRAND_ALIAS = os.environ.get("{PACKAGE}_ALIAS", "{alias}")
30
+
31
+ # Original values for replacement
32
+ _ORIGINAL_NAME = "{package}"
33
+ _ORIGINAL_ALIAS = "{alias}"
34
+
35
+
36
+ def _brand_to_env_prefix(brand: str) -> str:
37
+ """Convert brand name to environment variable prefix.
38
+
39
+ Examples:
40
+ "figrecipe" -> "FIGRECIPE"
41
+ "scitex.plt" -> "SCITEX_PLT"
42
+ "crossref-local" -> "CROSSREF_LOCAL"
43
+ """
44
+ return brand.upper().replace(".", "_").replace("-", "_")
45
+
46
+
47
+ # Environment variable prefix based on brand
48
+ ENV_PREFIX = _brand_to_env_prefix(BRAND_NAME)
49
+
50
+
51
+ def get_env(key: str, default: Optional[str] = None) -> Optional[str]:
52
+ """Get environment variable with brand-aware prefix.
53
+
54
+ Checks {ENV_PREFIX}_{key} first, then falls back to original prefix.
55
+ """
56
+ value = os.environ.get(f"{ENV_PREFIX}_{key}")
57
+ if value is not None:
58
+ return value
59
+
60
+ # Fall back to original prefix if different
61
+ original_prefix = _brand_to_env_prefix(_ORIGINAL_NAME)
62
+ if ENV_PREFIX != original_prefix:
63
+ value = os.environ.get(f"{original_prefix}_{key}")
64
+ if value is not None:
65
+ return value
66
+
67
+ return default
68
+
69
+
70
+ def rebrand_text(text: Optional[str]) -> Optional[str]:
71
+ """Apply branding to a text string (docstrings, error messages)."""
72
+ if text is None:
73
+ return None
74
+
75
+ if BRAND_NAME == _ORIGINAL_NAME and BRAND_ALIAS == _ORIGINAL_ALIAS:
76
+ return text
77
+
78
+ result = text
79
+
80
+ # Replace import statements
81
+ result = re.sub(
82
+ rf"import\s+{_ORIGINAL_NAME}\s+as\s+{_ORIGINAL_ALIAS}",
83
+ f"import {BRAND_NAME} as {BRAND_ALIAS}",
84
+ result,
85
+ )
86
+
87
+ # Replace "from package" statements
88
+ result = re.sub(
89
+ rf"from\s+{_ORIGINAL_NAME}(\s+import|\s*\.)",
90
+ lambda m: f"from {BRAND_NAME}{m.group(1)}",
91
+ result,
92
+ )
93
+
94
+ return result
95
+
96
+
97
+ def get_mcp_server_name() -> str:
98
+ """Get the MCP server name based on branding."""
99
+ return BRAND_NAME.replace(".", "-")
100
+ ```
101
+
102
+ ## Usage in Parent Package (scitex)
103
+
104
+ ```python
105
+ # scitex/plt/__init__.py
106
+ import os
107
+
108
+ # Set branding BEFORE importing the external package
109
+ os.environ["FIGRECIPE_BRAND"] = "scitex.plt"
110
+ os.environ["FIGRECIPE_ALIAS"] = "plt"
111
+
112
+ # Now import - docstrings will show scitex.plt instead of figrecipe
113
+ from figrecipe import *
114
+ ```
115
+
116
+ ## Port Scheme
117
+
118
+ SciTeX uses port scheme 3129X (sa-i-te-ku-su → 3-1-2-9 in Japanese):
119
+
120
+ | Port | Service |
121
+ |-------|------------------|
122
+ | 31290 | scitex-cloud |
123
+ | 31291 | crossref-local |
124
+ | 31292 | openalex |
125
+ | 31293 | scitex-audio |
126
+
127
+ ## Environment Variable Pattern
128
+
129
+ External packages should use `{ENV_PREFIX}_{SETTING}`:
130
+
131
+ ```
132
+ SCITEX_PLT_MODE=local
133
+ CROSSREF_LOCAL_API_URL=http://localhost:31291
134
+ SCITEX_AUDIO_RELAY_URL=http://localhost:31293
135
+ ```
136
+
137
+ ## Example: crossref-local
138
+
139
+ See GitHub Issue: https://github.com/ywatanabe1989/crossref-local/issues/11
140
+
141
+ The crossref-local package should implement:
142
+ - `CROSSREF_LOCAL_BRAND` / `CROSSREF_LOCAL_ALIAS` env vars
143
+ - Dynamic `ENV_PREFIX` derived from brand name
144
+ - When used via scitex.scholar, shows `scitex.scholar` in docs
145
+
146
+ ## References
147
+
148
+ - figrecipe/_branding.py - Reference implementation
149
+ - scitex/audio/_branding.py - Simple internal module (no rebranding needed)
@@ -1,7 +1,7 @@
1
1
  <!-- ---
2
2
  !-- Timestamp: 2025-12-08 23:59:46
3
3
  !-- Author: ywatanabe
4
- !-- File: /home/ywatanabe/proj/scitex-code/src/scitex/plt/gallery/README.md
4
+ !-- File: /home/ywatanabe/proj/scitex-python/src/scitex/plt/gallery/README.md
5
5
  !-- --- -->
6
6
 
7
7
  # SciTeX Plot Gallery
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: "2026-01-24 (ywatanabe)"
3
+ # File: /home/ywatanabe/proj/scitex-python/src/scitex/plt/utils/_hitmap/__init__.py
4
+
5
+ """
6
+ Hit map generation utilities for interactive element selection.
7
+
8
+ This package provides functions to generate hit maps for matplotlib figures,
9
+ enabling pixel-perfect element selection in web editors and interactive tools.
10
+
11
+ Supported methods:
12
+ 1. ID Colors: Single render with unique colors per element (~89ms)
13
+ 2. Export Path Data: Extract geometry for client-side hit testing (~192ms)
14
+
15
+ Reserved colors:
16
+ - Black (#000000, ID=0): Background/no element
17
+ - Dark gray (#010101, ID=65793): Non-selectable axes elements (spines, labels, ticks)
18
+ """
19
+
20
+ from ._artist_extraction import (
21
+ detect_logical_groups,
22
+ get_all_artists,
23
+ get_all_artists_with_groups,
24
+ )
25
+ from ._color_application import (
26
+ apply_hitmap_colors,
27
+ apply_id_color,
28
+ prepare_hitmap_figure,
29
+ restore_figure_props,
30
+ restore_original_colors,
31
+ )
32
+ from ._color_conversion import id_to_rgb, rgb_to_id, rgb_to_id_lookup
33
+ from ._constants import HITMAP_AXES_COLOR, HITMAP_BACKGROUND_COLOR, to_native
34
+ from ._hitmap_core import generate_hitmap_id_colors, generate_hitmap_with_bbox_tight
35
+ from ._path_extraction import extract_path_data, extract_selectable_regions
36
+ from ._query import query_hitmap_neighborhood, save_hitmap_png
37
+
38
+ # Backward compatibility aliases
39
+ _to_native = to_native
40
+ _id_to_rgb = id_to_rgb
41
+ _rgb_to_id = rgb_to_id
42
+ _rgb_to_id_lookup = rgb_to_id_lookup
43
+ _apply_id_color = apply_id_color
44
+ _prepare_hitmap_figure = prepare_hitmap_figure
45
+ _restore_figure_props = restore_figure_props
46
+
47
+ __all__ = [
48
+ # Constants
49
+ "HITMAP_BACKGROUND_COLOR",
50
+ "HITMAP_AXES_COLOR",
51
+ # Artist extraction
52
+ "get_all_artists",
53
+ "get_all_artists_with_groups",
54
+ "detect_logical_groups",
55
+ # Color conversion
56
+ "id_to_rgb",
57
+ "rgb_to_id",
58
+ "rgb_to_id_lookup",
59
+ # Core hitmap generation
60
+ "generate_hitmap_id_colors",
61
+ "generate_hitmap_with_bbox_tight",
62
+ # Path extraction
63
+ "extract_path_data",
64
+ "extract_selectable_regions",
65
+ # Query and save
66
+ "query_hitmap_neighborhood",
67
+ "save_hitmap_png",
68
+ # Color application
69
+ "apply_hitmap_colors",
70
+ "restore_original_colors",
71
+ # Backward compatibility
72
+ "_to_native",
73
+ "_id_to_rgb",
74
+ "_rgb_to_id",
75
+ "_rgb_to_id_lookup",
76
+ "_apply_id_color",
77
+ "_prepare_hitmap_figure",
78
+ "_restore_figure_props",
79
+ ]
80
+
81
+
82
+ # EOF
@@ -0,0 +1,343 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: "2026-01-24 (ywatanabe)"
3
+ # File: /home/ywatanabe/proj/scitex-python/src/scitex/plt/utils/_hitmap/_artist_extraction.py
4
+
5
+ """
6
+ Artist extraction functions for hitmap generation.
7
+
8
+ This module provides functions to extract selectable artists from matplotlib
9
+ figures and detect logical groups (histogram, bar series, etc.).
10
+ """
11
+
12
+ from typing import Any, Dict, List, Optional, Tuple
13
+
14
+ __all__ = [
15
+ "get_all_artists",
16
+ "get_all_artists_with_groups",
17
+ "detect_logical_groups",
18
+ ]
19
+
20
+
21
+ def get_all_artists(fig, include_text: bool = False) -> List[Tuple[Any, int, str]]:
22
+ """
23
+ Extract all selectable artists from a figure.
24
+
25
+ Parameters
26
+ ----------
27
+ fig : matplotlib.figure.Figure
28
+ The figure to extract artists from.
29
+ include_text : bool
30
+ Whether to include text elements.
31
+
32
+ Returns
33
+ -------
34
+ list of tuple
35
+ List of (artist, axes_index, artist_type) tuples.
36
+ """
37
+ artists = []
38
+
39
+ for ax_idx, ax in enumerate(fig.axes):
40
+ # Lines (Line2D)
41
+ for line in ax.get_lines():
42
+ label = line.get_label()
43
+ if not label.startswith("_"): # Skip internal lines
44
+ artists.append((line, ax_idx, "line"))
45
+
46
+ # Scatter plots (PathCollection)
47
+ for coll in ax.collections:
48
+ coll_type = type(coll).__name__
49
+ if "PathCollection" in coll_type:
50
+ artists.append((coll, ax_idx, "scatter"))
51
+ elif "PolyCollection" in coll_type or "FillBetween" in coll_type:
52
+ artists.append((coll, ax_idx, "fill"))
53
+ elif "QuadMesh" in coll_type:
54
+ artists.append((coll, ax_idx, "mesh"))
55
+
56
+ # Bars (Rectangle patches in containers)
57
+ for container in ax.containers:
58
+ if hasattr(container, "patches") and container.patches:
59
+ artists.append((container, ax_idx, "bar"))
60
+
61
+ # Individual patches (rectangles, circles, etc.)
62
+ for patch in ax.patches:
63
+ patch_type = type(patch).__name__
64
+ if patch_type == "Rectangle":
65
+ artists.append((patch, ax_idx, "rectangle"))
66
+ elif patch_type in ("Circle", "Ellipse"):
67
+ artists.append((patch, ax_idx, "circle"))
68
+ elif patch_type == "Polygon":
69
+ artists.append((patch, ax_idx, "polygon"))
70
+
71
+ # Images
72
+ for img in ax.images:
73
+ artists.append((img, ax_idx, "image"))
74
+
75
+ # Text (optional)
76
+ if include_text:
77
+ for text in ax.texts:
78
+ if text.get_text():
79
+ artists.append((text, ax_idx, "text"))
80
+
81
+ return artists
82
+
83
+
84
+ def detect_logical_groups(fig) -> Dict[str, Dict[str, Any]]:
85
+ """
86
+ Detect logical groups in a matplotlib figure.
87
+
88
+ Logical groups represent high-level plot elements that may consist of
89
+ multiple physical matplotlib artists. For example:
90
+ - Histogram: Many Rectangle patches grouped as one "histogram"
91
+ - Bar series: BarContainer with multiple bars
92
+ - Box plot: Box, whiskers, caps, median, fliers as one "boxplot"
93
+ - Error bars: Line + error caps as one "errorbar"
94
+
95
+ Parameters
96
+ ----------
97
+ fig : matplotlib.figure.Figure
98
+ The figure to analyze.
99
+
100
+ Returns
101
+ -------
102
+ dict
103
+ Dictionary mapping group_id to group info.
104
+ """
105
+ groups = {}
106
+ group_counter = {}
107
+
108
+ def get_group_id(group_type: str, ax_idx: int) -> str:
109
+ """Generate unique group ID."""
110
+ key = f"{group_type}_{ax_idx}"
111
+ if key not in group_counter:
112
+ group_counter[key] = 0
113
+ idx = group_counter[key]
114
+ group_counter[key] += 1
115
+ return f"{group_type}_{ax_idx}_{idx}"
116
+
117
+ for ax_idx, ax in enumerate(fig.axes):
118
+ # Detect BarContainers (covers bar charts and histograms)
119
+ bar_containers = [
120
+ c for c in ax.containers if "BarContainer" in type(c).__name__
121
+ ]
122
+ n_bar_containers = len(bar_containers)
123
+
124
+ for container in ax.containers:
125
+ container_type = type(container).__name__
126
+
127
+ if "BarContainer" in container_type:
128
+ patches = (
129
+ list(container.patches) if hasattr(container, "patches") else []
130
+ )
131
+ if not patches:
132
+ continue
133
+
134
+ # Check if bars are adjacent (histogram) or spaced (bar chart)
135
+ is_histogram = False
136
+ if len(patches) > 1:
137
+ widths = [p.get_width() for p in patches]
138
+ x_positions = [p.get_x() for p in patches]
139
+ if len(x_positions) > 1:
140
+ gaps = [
141
+ x_positions[i + 1] - (x_positions[i] + widths[i])
142
+ for i in range(len(x_positions) - 1)
143
+ ]
144
+ avg_width = sum(widths) / len(widths)
145
+ is_histogram = all(abs(g) < avg_width * 0.1 for g in gaps)
146
+
147
+ if is_histogram:
148
+ group_type = "histogram"
149
+ group_id = get_group_id(group_type, ax_idx)
150
+ label = ""
151
+ if hasattr(container, "get_label"):
152
+ label = container.get_label()
153
+ if not label or label.startswith("_"):
154
+ label = f"{group_type}_{len([g for g in groups if group_type in g])}"
155
+
156
+ groups[group_id] = {
157
+ "type": group_type,
158
+ "label": label,
159
+ "axes_index": ax_idx,
160
+ "artists": patches,
161
+ "artist_types": ["rectangle"] * len(patches),
162
+ "role": "logical",
163
+ "member_count": len(patches),
164
+ }
165
+
166
+ elif n_bar_containers > 1:
167
+ group_type = "bar_series"
168
+ group_id = get_group_id(group_type, ax_idx)
169
+ label = ""
170
+ if hasattr(container, "get_label"):
171
+ label = container.get_label()
172
+ if not label or label.startswith("_"):
173
+ label = f"{group_type}_{len([g for g in groups if group_type in g])}"
174
+
175
+ groups[group_id] = {
176
+ "type": group_type,
177
+ "label": label,
178
+ "axes_index": ax_idx,
179
+ "artists": patches,
180
+ "artist_types": ["rectangle"] * len(patches),
181
+ "role": "logical",
182
+ "member_count": len(patches),
183
+ }
184
+
185
+ elif "ErrorbarContainer" in container_type:
186
+ group_id = get_group_id("errorbar", ax_idx)
187
+ artists = []
188
+ artist_types = []
189
+
190
+ if hasattr(container, "lines"):
191
+ data_line, caplines, barlinecols = container.lines
192
+ if data_line:
193
+ artists.append(data_line)
194
+ artist_types.append("line")
195
+ artists.extend(caplines)
196
+ artist_types.extend(["line"] * len(caplines))
197
+ artists.extend(barlinecols)
198
+ artist_types.extend(["line_collection"] * len(barlinecols))
199
+
200
+ label = container.get_label() if hasattr(container, "get_label") else ""
201
+ if not label or label.startswith("_"):
202
+ label = f"errorbar_{len([g for g in groups if 'errorbar' in g])}"
203
+
204
+ groups[group_id] = {
205
+ "type": "errorbar",
206
+ "label": label,
207
+ "axes_index": ax_idx,
208
+ "artists": artists,
209
+ "artist_types": artist_types,
210
+ "role": "logical",
211
+ "member_count": len(artists),
212
+ }
213
+
214
+ # Detect pie charts (Wedge patches)
215
+ wedges = [p for p in ax.patches if type(p).__name__ == "Wedge"]
216
+ if wedges:
217
+ group_id = get_group_id("pie", ax_idx)
218
+ groups[group_id] = {
219
+ "type": "pie",
220
+ "label": "Pie Chart",
221
+ "axes_index": ax_idx,
222
+ "artists": wedges,
223
+ "artist_types": ["wedge"] * len(wedges),
224
+ "role": "logical",
225
+ "member_count": len(wedges),
226
+ }
227
+
228
+ # Detect contour sets
229
+ poly_collections = [
230
+ c
231
+ for c in ax.collections
232
+ if "PolyCollection" in type(c).__name__
233
+ and hasattr(c, "get_array")
234
+ and c.get_array() is not None
235
+ ]
236
+ if len(poly_collections) > 2:
237
+ group_id = get_group_id("contour", ax_idx)
238
+ groups[group_id] = {
239
+ "type": "contour",
240
+ "label": "Contour Plot",
241
+ "axes_index": ax_idx,
242
+ "artists": poly_collections,
243
+ "artist_types": ["poly_collection"] * len(poly_collections),
244
+ "role": "logical",
245
+ "member_count": len(poly_collections),
246
+ }
247
+
248
+ return groups
249
+
250
+
251
+ def get_all_artists_with_groups(
252
+ fig, include_text: bool = False
253
+ ) -> Tuple[List[Tuple[Any, int, str, Optional[str]]], Dict[str, Dict[str, Any]]]:
254
+ """
255
+ Extract all selectable artists from a figure with logical group information.
256
+
257
+ Parameters
258
+ ----------
259
+ fig : matplotlib.figure.Figure
260
+ The figure to extract artists from.
261
+ include_text : bool
262
+ Whether to include text elements.
263
+
264
+ Returns
265
+ -------
266
+ tuple
267
+ (artists_list, groups_dict) where:
268
+ - artists_list: List of (artist, axes_index, artist_type, group_id) tuples
269
+ - groups_dict: Dictionary of logical groups
270
+ """
271
+ groups = detect_logical_groups(fig)
272
+
273
+ artist_to_group = {}
274
+ for group_id, group_info in groups.items():
275
+ for artist in group_info["artists"]:
276
+ artist_to_group[id(artist)] = group_id
277
+
278
+ artists_with_groups = []
279
+
280
+ for ax_idx, ax in enumerate(fig.axes):
281
+ # Lines
282
+ for line in ax.get_lines():
283
+ label = line.get_label()
284
+ if not label.startswith("_"):
285
+ group_id = artist_to_group.get(id(line))
286
+ artists_with_groups.append((line, ax_idx, "line", group_id))
287
+
288
+ # Collections
289
+ for coll in ax.collections:
290
+ coll_type = type(coll).__name__
291
+ group_id = artist_to_group.get(id(coll))
292
+ if "PathCollection" in coll_type:
293
+ artists_with_groups.append((coll, ax_idx, "scatter", group_id))
294
+ elif "PolyCollection" in coll_type or "FillBetween" in coll_type:
295
+ artists_with_groups.append((coll, ax_idx, "fill", group_id))
296
+ elif "QuadMesh" in coll_type:
297
+ artists_with_groups.append((coll, ax_idx, "mesh", group_id))
298
+
299
+ # Bars
300
+ processed_patches = set()
301
+ for container in ax.containers:
302
+ if hasattr(container, "patches") and container.patches:
303
+ group_id = artist_to_group.get(id(container.patches[0]))
304
+ if group_id:
305
+ artists_with_groups.append((container, ax_idx, "bar", group_id))
306
+ for patch in container.patches:
307
+ processed_patches.add(id(patch))
308
+ else:
309
+ for patch in container.patches:
310
+ artists_with_groups.append((patch, ax_idx, "rectangle", None))
311
+ processed_patches.add(id(patch))
312
+
313
+ # Patches
314
+ for patch in ax.patches:
315
+ if id(patch) in processed_patches:
316
+ continue
317
+ patch_type = type(patch).__name__
318
+ group_id = artist_to_group.get(id(patch))
319
+ if patch_type == "Rectangle":
320
+ artists_with_groups.append((patch, ax_idx, "rectangle", group_id))
321
+ elif patch_type in ("Circle", "Ellipse"):
322
+ artists_with_groups.append((patch, ax_idx, "circle", group_id))
323
+ elif patch_type == "Polygon":
324
+ artists_with_groups.append((patch, ax_idx, "polygon", group_id))
325
+ elif patch_type == "Wedge":
326
+ artists_with_groups.append((patch, ax_idx, "wedge", group_id))
327
+
328
+ # Images
329
+ for img in ax.images:
330
+ group_id = artist_to_group.get(id(img))
331
+ artists_with_groups.append((img, ax_idx, "image", group_id))
332
+
333
+ # Text
334
+ if include_text:
335
+ for text in ax.texts:
336
+ if text.get_text():
337
+ group_id = artist_to_group.get(id(text))
338
+ artists_with_groups.append((text, ax_idx, "text", group_id))
339
+
340
+ return artists_with_groups, groups
341
+
342
+
343
+ # EOF