scitex 2.14.0__py3-none-any.whl → 2.15.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 (300) hide show
  1. scitex/__init__.py +71 -17
  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 +210 -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/template.py +24 -0
  17. scitex/_mcp_tools/writer.py +21 -204
  18. scitex/ai/_gen_ai/_PARAMS.py +10 -7
  19. scitex/ai/classification/reporters/_SingleClassificationReporter.py +45 -1603
  20. scitex/ai/classification/reporters/_mixins/__init__.py +36 -0
  21. scitex/ai/classification/reporters/_mixins/_constants.py +67 -0
  22. scitex/ai/classification/reporters/_mixins/_cv_summary.py +387 -0
  23. scitex/ai/classification/reporters/_mixins/_feature_importance.py +119 -0
  24. scitex/ai/classification/reporters/_mixins/_metrics.py +275 -0
  25. scitex/ai/classification/reporters/_mixins/_plotting.py +179 -0
  26. scitex/ai/classification/reporters/_mixins/_reports.py +153 -0
  27. scitex/ai/classification/reporters/_mixins/_storage.py +160 -0
  28. scitex/ai/classification/timeseries/_TimeSeriesSlidingWindowSplit.py +30 -1550
  29. scitex/ai/classification/timeseries/_sliding_window_core.py +467 -0
  30. scitex/ai/classification/timeseries/_sliding_window_plotting.py +369 -0
  31. scitex/audio/README.md +40 -36
  32. scitex/audio/__init__.py +129 -61
  33. scitex/audio/_branding.py +185 -0
  34. scitex/audio/_mcp/__init__.py +32 -0
  35. scitex/audio/_mcp/handlers.py +59 -6
  36. scitex/audio/_mcp/speak_handlers.py +238 -0
  37. scitex/audio/_relay.py +225 -0
  38. scitex/audio/_tts.py +18 -10
  39. scitex/audio/engines/base.py +17 -10
  40. scitex/audio/engines/elevenlabs_engine.py +7 -2
  41. scitex/audio/mcp_server.py +228 -75
  42. scitex/canvas/README.md +1 -1
  43. scitex/canvas/editor/_dearpygui/__init__.py +25 -0
  44. scitex/canvas/editor/_dearpygui/_editor.py +147 -0
  45. scitex/canvas/editor/_dearpygui/_handlers.py +476 -0
  46. scitex/canvas/editor/_dearpygui/_panels/__init__.py +17 -0
  47. scitex/canvas/editor/_dearpygui/_panels/_control.py +119 -0
  48. scitex/canvas/editor/_dearpygui/_panels/_element_controls.py +190 -0
  49. scitex/canvas/editor/_dearpygui/_panels/_preview.py +43 -0
  50. scitex/canvas/editor/_dearpygui/_panels/_sections.py +390 -0
  51. scitex/canvas/editor/_dearpygui/_plotting.py +187 -0
  52. scitex/canvas/editor/_dearpygui/_rendering.py +504 -0
  53. scitex/canvas/editor/_dearpygui/_selection.py +295 -0
  54. scitex/canvas/editor/_dearpygui/_state.py +93 -0
  55. scitex/canvas/editor/_dearpygui/_utils.py +61 -0
  56. scitex/canvas/editor/flask_editor/_core/__init__.py +27 -0
  57. scitex/canvas/editor/flask_editor/_core/_bbox_extraction.py +200 -0
  58. scitex/canvas/editor/flask_editor/_core/_editor.py +173 -0
  59. scitex/canvas/editor/flask_editor/_core/_export_helpers.py +353 -0
  60. scitex/canvas/editor/flask_editor/_core/_routes_basic.py +190 -0
  61. scitex/canvas/editor/flask_editor/_core/_routes_export.py +332 -0
  62. scitex/canvas/editor/flask_editor/_core/_routes_panels.py +252 -0
  63. scitex/canvas/editor/flask_editor/_core/_routes_save.py +218 -0
  64. scitex/canvas/editor/flask_editor/_core.py +25 -1684
  65. scitex/canvas/editor/flask_editor/templates/__init__.py +32 -70
  66. scitex/cli/__init__.py +38 -43
  67. scitex/cli/audio.py +76 -27
  68. scitex/cli/capture.py +13 -20
  69. scitex/cli/introspect.py +481 -0
  70. scitex/cli/main.py +200 -109
  71. scitex/cli/mcp.py +60 -34
  72. scitex/cli/plt.py +357 -0
  73. scitex/cli/repro.py +15 -8
  74. scitex/cli/resource.py +15 -8
  75. scitex/cli/scholar/__init__.py +23 -8
  76. scitex/cli/scholar/_crossref_scitex.py +296 -0
  77. scitex/cli/scholar/_fetch.py +25 -3
  78. scitex/cli/social.py +314 -0
  79. scitex/cli/stats.py +15 -8
  80. scitex/cli/template.py +129 -12
  81. scitex/cli/tex.py +15 -8
  82. scitex/cli/writer.py +132 -8
  83. scitex/cloud/__init__.py +41 -2
  84. scitex/config/README.md +1 -1
  85. scitex/config/__init__.py +16 -2
  86. scitex/config/_env_registry.py +256 -0
  87. scitex/context/__init__.py +22 -0
  88. scitex/dev/__init__.py +20 -1
  89. scitex/diagram/__init__.py +42 -19
  90. scitex/diagram/mcp_server.py +13 -125
  91. scitex/gen/__init__.py +50 -14
  92. scitex/gen/_list_packages.py +4 -4
  93. scitex/introspect/__init__.py +82 -0
  94. scitex/introspect/_call_graph.py +303 -0
  95. scitex/introspect/_class_hierarchy.py +163 -0
  96. scitex/introspect/_core.py +41 -0
  97. scitex/introspect/_docstring.py +131 -0
  98. scitex/introspect/_examples.py +113 -0
  99. scitex/introspect/_imports.py +271 -0
  100. scitex/{gen/_inspect_module.py → introspect/_list_api.py} +43 -54
  101. scitex/introspect/_mcp/__init__.py +41 -0
  102. scitex/introspect/_mcp/handlers.py +233 -0
  103. scitex/introspect/_members.py +155 -0
  104. scitex/introspect/_resolve.py +89 -0
  105. scitex/introspect/_signature.py +131 -0
  106. scitex/introspect/_source.py +80 -0
  107. scitex/introspect/_type_hints.py +172 -0
  108. scitex/io/_save.py +1 -2
  109. scitex/io/bundle/README.md +1 -1
  110. scitex/logging/_formatters.py +19 -9
  111. scitex/mcp_server.py +98 -5
  112. scitex/os/__init__.py +4 -0
  113. scitex/{gen → os}/_check_host.py +4 -5
  114. scitex/plt/__init__.py +245 -550
  115. scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin/_wrappers.py +5 -10
  116. scitex/plt/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
  117. scitex/plt/gallery/README.md +1 -1
  118. scitex/plt/utils/_hitmap/__init__.py +82 -0
  119. scitex/plt/utils/_hitmap/_artist_extraction.py +343 -0
  120. scitex/plt/utils/_hitmap/_color_application.py +346 -0
  121. scitex/plt/utils/_hitmap/_color_conversion.py +121 -0
  122. scitex/plt/utils/_hitmap/_constants.py +40 -0
  123. scitex/plt/utils/_hitmap/_hitmap_core.py +334 -0
  124. scitex/plt/utils/_hitmap/_path_extraction.py +357 -0
  125. scitex/plt/utils/_hitmap/_query.py +113 -0
  126. scitex/plt/utils/_hitmap.py +46 -1616
  127. scitex/plt/utils/_metadata/__init__.py +80 -0
  128. scitex/plt/utils/_metadata/_artists/__init__.py +25 -0
  129. scitex/plt/utils/_metadata/_artists/_base.py +195 -0
  130. scitex/plt/utils/_metadata/_artists/_collections.py +356 -0
  131. scitex/plt/utils/_metadata/_artists/_extract.py +57 -0
  132. scitex/plt/utils/_metadata/_artists/_images.py +80 -0
  133. scitex/plt/utils/_metadata/_artists/_lines.py +261 -0
  134. scitex/plt/utils/_metadata/_artists/_patches.py +247 -0
  135. scitex/plt/utils/_metadata/_artists/_text.py +106 -0
  136. scitex/plt/utils/_metadata/_csv.py +416 -0
  137. scitex/plt/utils/_metadata/_detect.py +225 -0
  138. scitex/plt/utils/_metadata/_legend.py +127 -0
  139. scitex/plt/utils/_metadata/_rounding.py +117 -0
  140. scitex/plt/utils/_metadata/_verification.py +202 -0
  141. scitex/schema/README.md +1 -1
  142. scitex/scholar/__init__.py +8 -0
  143. scitex/scholar/_mcp/crossref_handlers.py +265 -0
  144. scitex/scholar/core/Scholar.py +63 -1700
  145. scitex/scholar/core/_mixins/__init__.py +36 -0
  146. scitex/scholar/core/_mixins/_enrichers.py +270 -0
  147. scitex/scholar/core/_mixins/_library_handlers.py +100 -0
  148. scitex/scholar/core/_mixins/_loaders.py +103 -0
  149. scitex/scholar/core/_mixins/_pdf_download.py +375 -0
  150. scitex/scholar/core/_mixins/_pipeline.py +312 -0
  151. scitex/scholar/core/_mixins/_project_handlers.py +125 -0
  152. scitex/scholar/core/_mixins/_savers.py +69 -0
  153. scitex/scholar/core/_mixins/_search.py +103 -0
  154. scitex/scholar/core/_mixins/_services.py +88 -0
  155. scitex/scholar/core/_mixins/_url_finding.py +105 -0
  156. scitex/scholar/crossref_scitex.py +367 -0
  157. scitex/scholar/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
  158. scitex/scholar/examples/00_run_all.sh +120 -0
  159. scitex/scholar/jobs/_executors.py +27 -3
  160. scitex/scholar/pdf_download/ScholarPDFDownloader.py +38 -416
  161. scitex/scholar/pdf_download/_cli.py +154 -0
  162. scitex/scholar/pdf_download/strategies/__init__.py +11 -8
  163. scitex/scholar/pdf_download/strategies/manual_download_fallback.py +80 -3
  164. scitex/scholar/pipelines/ScholarPipelineBibTeX.py +73 -121
  165. scitex/scholar/pipelines/ScholarPipelineParallel.py +80 -138
  166. scitex/scholar/pipelines/ScholarPipelineSingle.py +43 -63
  167. scitex/scholar/pipelines/_single_steps.py +71 -36
  168. scitex/scholar/storage/_LibraryManager.py +97 -1695
  169. scitex/scholar/storage/_mixins/__init__.py +30 -0
  170. scitex/scholar/storage/_mixins/_bibtex_handlers.py +128 -0
  171. scitex/scholar/storage/_mixins/_library_operations.py +218 -0
  172. scitex/scholar/storage/_mixins/_metadata_conversion.py +226 -0
  173. scitex/scholar/storage/_mixins/_paper_saving.py +456 -0
  174. scitex/scholar/storage/_mixins/_resolution.py +376 -0
  175. scitex/scholar/storage/_mixins/_storage_helpers.py +121 -0
  176. scitex/scholar/storage/_mixins/_symlink_handlers.py +226 -0
  177. scitex/scholar/url_finder/.tmp/open_url/KNOWN_RESOLVERS.py +462 -0
  178. scitex/scholar/url_finder/.tmp/open_url/README.md +223 -0
  179. scitex/scholar/url_finder/.tmp/open_url/_DOIToURLResolver.py +694 -0
  180. scitex/scholar/url_finder/.tmp/open_url/_OpenURLResolver.py +1160 -0
  181. scitex/scholar/url_finder/.tmp/open_url/_ResolverLinkFinder.py +344 -0
  182. scitex/scholar/url_finder/.tmp/open_url/__init__.py +24 -0
  183. scitex/security/README.md +3 -3
  184. scitex/session/README.md +1 -1
  185. scitex/session/__init__.py +26 -7
  186. scitex/session/_decorator.py +1 -1
  187. scitex/sh/README.md +1 -1
  188. scitex/sh/__init__.py +7 -4
  189. scitex/social/__init__.py +155 -0
  190. scitex/social/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
  191. scitex/stats/_mcp/_handlers/__init__.py +31 -0
  192. scitex/stats/_mcp/_handlers/_corrections.py +113 -0
  193. scitex/stats/_mcp/_handlers/_descriptive.py +78 -0
  194. scitex/stats/_mcp/_handlers/_effect_size.py +106 -0
  195. scitex/stats/_mcp/_handlers/_format.py +94 -0
  196. scitex/stats/_mcp/_handlers/_normality.py +110 -0
  197. scitex/stats/_mcp/_handlers/_posthoc.py +224 -0
  198. scitex/stats/_mcp/_handlers/_power.py +247 -0
  199. scitex/stats/_mcp/_handlers/_recommend.py +102 -0
  200. scitex/stats/_mcp/_handlers/_run_test.py +279 -0
  201. scitex/stats/_mcp/_handlers/_stars.py +48 -0
  202. scitex/stats/_mcp/handlers.py +19 -1171
  203. scitex/stats/auto/_stat_style.py +175 -0
  204. scitex/stats/auto/_style_definitions.py +411 -0
  205. scitex/stats/auto/_styles.py +22 -620
  206. scitex/stats/descriptive/__init__.py +11 -8
  207. scitex/stats/descriptive/_ci.py +39 -0
  208. scitex/stats/power/_power.py +15 -4
  209. scitex/str/__init__.py +2 -1
  210. scitex/str/_title_case.py +63 -0
  211. scitex/template/README.md +1 -1
  212. scitex/template/__init__.py +25 -10
  213. scitex/template/_code_templates.py +147 -0
  214. scitex/template/_mcp/handlers.py +81 -0
  215. scitex/template/_mcp/tool_schemas.py +55 -0
  216. scitex/template/_templates/__init__.py +51 -0
  217. scitex/template/_templates/audio.py +233 -0
  218. scitex/template/_templates/canvas.py +312 -0
  219. scitex/template/_templates/capture.py +268 -0
  220. scitex/template/_templates/config.py +43 -0
  221. scitex/template/_templates/diagram.py +294 -0
  222. scitex/template/_templates/io.py +107 -0
  223. scitex/template/_templates/module.py +53 -0
  224. scitex/template/_templates/plt.py +202 -0
  225. scitex/template/_templates/scholar.py +267 -0
  226. scitex/template/_templates/session.py +130 -0
  227. scitex/template/_templates/session_minimal.py +43 -0
  228. scitex/template/_templates/session_plot.py +67 -0
  229. scitex/template/_templates/session_stats.py +77 -0
  230. scitex/template/_templates/stats.py +323 -0
  231. scitex/template/_templates/writer.py +296 -0
  232. scitex/template/clone_writer_directory.py +5 -5
  233. scitex/ui/_backends/_email.py +10 -2
  234. scitex/ui/_backends/_webhook.py +5 -1
  235. scitex/web/_search_pubmed.py +10 -6
  236. scitex/writer/README.md +1 -1
  237. scitex/writer/_mcp/handlers.py +11 -744
  238. scitex/writer/_mcp/tool_schemas.py +5 -335
  239. scitex-2.15.2.dist-info/METADATA +648 -0
  240. {scitex-2.14.0.dist-info → scitex-2.15.2.dist-info}/RECORD +246 -150
  241. scitex/canvas/editor/flask_editor/templates/_scripts.py +0 -4933
  242. scitex/canvas/editor/flask_editor/templates/_styles.py +0 -1658
  243. scitex/dev/plt/data/mpl/PLOTTING_FUNCTIONS.yaml +0 -90
  244. scitex/dev/plt/data/mpl/PLOTTING_SIGNATURES.yaml +0 -1571
  245. scitex/dev/plt/data/mpl/PLOTTING_SIGNATURES_DETAILED.yaml +0 -6262
  246. scitex/dev/plt/data/mpl/SIGNATURES_FLATTENED.yaml +0 -1274
  247. scitex/dev/plt/data/mpl/dir_ax.txt +0 -459
  248. scitex/diagram/_compile.py +0 -312
  249. scitex/diagram/_diagram.py +0 -355
  250. scitex/diagram/_mcp/__init__.py +0 -4
  251. scitex/diagram/_mcp/handlers.py +0 -400
  252. scitex/diagram/_mcp/tool_schemas.py +0 -157
  253. scitex/diagram/_presets.py +0 -173
  254. scitex/diagram/_schema.py +0 -182
  255. scitex/diagram/_split.py +0 -278
  256. scitex/gen/_ci.py +0 -12
  257. scitex/gen/_title_case.py +0 -89
  258. scitex/plt/_mcp/__init__.py +0 -4
  259. scitex/plt/_mcp/_handlers_annotation.py +0 -102
  260. scitex/plt/_mcp/_handlers_figure.py +0 -195
  261. scitex/plt/_mcp/_handlers_plot.py +0 -252
  262. scitex/plt/_mcp/_handlers_style.py +0 -219
  263. scitex/plt/_mcp/handlers.py +0 -74
  264. scitex/plt/_mcp/tool_schemas.py +0 -497
  265. scitex/plt/mcp_server.py +0 -231
  266. scitex/scholar/data/.gitkeep +0 -0
  267. scitex/scholar/data/README.md +0 -44
  268. scitex/scholar/data/bib_files/bibliography.bib +0 -1952
  269. scitex/scholar/data/bib_files/neurovista.bib +0 -277
  270. scitex/scholar/data/bib_files/neurovista_enriched.bib +0 -441
  271. scitex/scholar/data/bib_files/neurovista_enriched_enriched.bib +0 -441
  272. scitex/scholar/data/bib_files/neurovista_processed.bib +0 -338
  273. scitex/scholar/data/bib_files/openaccess.bib +0 -89
  274. scitex/scholar/data/bib_files/pac-seizure_prediction_enriched.bib +0 -2178
  275. scitex/scholar/data/bib_files/pac.bib +0 -698
  276. scitex/scholar/data/bib_files/pac_enriched.bib +0 -1061
  277. scitex/scholar/data/bib_files/pac_processed.bib +0 -0
  278. scitex/scholar/data/bib_files/pac_titles.txt +0 -75
  279. scitex/scholar/data/bib_files/paywalled.bib +0 -98
  280. scitex/scholar/data/bib_files/related-papers-by-coauthors.bib +0 -58
  281. scitex/scholar/data/bib_files/related-papers-by-coauthors_enriched.bib +0 -87
  282. scitex/scholar/data/bib_files/seizure_prediction.bib +0 -694
  283. scitex/scholar/data/bib_files/seizure_prediction_processed.bib +0 -0
  284. scitex/scholar/data/bib_files/test_complete_enriched.bib +0 -437
  285. scitex/scholar/data/bib_files/test_final_enriched.bib +0 -437
  286. scitex/scholar/data/bib_files/test_seizure.bib +0 -46
  287. scitex/scholar/data/impact_factor/JCR_IF_2022.xlsx +0 -0
  288. scitex/scholar/data/impact_factor/JCR_IF_2024.db +0 -0
  289. scitex/scholar/data/impact_factor/JCR_IF_2024.xlsx +0 -0
  290. scitex/scholar/data/impact_factor/JCR_IF_2024_v01.db +0 -0
  291. scitex/scholar/data/impact_factor.db +0 -0
  292. scitex/scholar/examples/SUGGESTIONS.md +0 -865
  293. scitex/scholar/examples/dev.py +0 -38
  294. scitex-2.14.0.dist-info/METADATA +0 -1238
  295. /scitex/{gen → context}/_detect_environment.py +0 -0
  296. /scitex/{gen → context}/_get_notebook_path.py +0 -0
  297. /scitex/{gen/_shell.py → sh/_shell_legacy.py} +0 -0
  298. {scitex-2.14.0.dist-info → scitex-2.15.2.dist-info}/WHEEL +0 -0
  299. {scitex-2.14.0.dist-info → scitex-2.15.2.dist-info}/entry_points.txt +0 -0
  300. {scitex-2.14.0.dist-info → scitex-2.15.2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,334 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: "2026-01-24 (ywatanabe)"
3
+ # File: /home/ywatanabe/proj/scitex-python/src/scitex/plt/utils/_hitmap/_hitmap_core.py
4
+
5
+ """
6
+ Core hitmap generation functions.
7
+
8
+ This module provides the main hitmap generation functions using unique ID colors
9
+ for pixel-perfect element selection.
10
+ """
11
+
12
+ import io
13
+ from typing import TYPE_CHECKING, Any, Dict, Tuple
14
+
15
+ import numpy as np
16
+
17
+ from ._artist_extraction import get_all_artists
18
+ from ._color_application import apply_id_color
19
+ from ._color_conversion import id_to_rgb
20
+ from ._constants import HITMAP_AXES_COLOR, HITMAP_BACKGROUND_COLOR
21
+
22
+ if TYPE_CHECKING:
23
+ from PIL import Image
24
+
25
+ __all__ = [
26
+ "generate_hitmap_id_colors",
27
+ "generate_hitmap_with_bbox_tight",
28
+ ]
29
+
30
+
31
+ def generate_hitmap_id_colors(
32
+ fig,
33
+ dpi: int = 100,
34
+ include_text: bool = False,
35
+ ) -> Tuple[np.ndarray, Dict[int, Dict[str, Any]]]:
36
+ """
37
+ Generate a hit map using unique ID colors (fastest method).
38
+
39
+ Assigns unique RGB colors to each element, renders once, and creates
40
+ a pixel-perfect hit map where each pixel's RGB values encode the
41
+ element ID using 24-bit color space (~16.7M unique IDs).
42
+
43
+ Parameters
44
+ ----------
45
+ fig : matplotlib.figure.Figure
46
+ The figure to generate hit map for.
47
+ dpi : int
48
+ Resolution for hit map rendering.
49
+ include_text : bool
50
+ Whether to include text elements in hit map.
51
+
52
+ Returns
53
+ -------
54
+ tuple
55
+ (hitmap_array, color_map) where:
56
+ - hitmap_array: uint32 array with element IDs (0 = background)
57
+ - color_map: dict mapping ID to element info
58
+
59
+ Notes
60
+ -----
61
+ Performance: ~89ms for complex figures (33x faster than sequential)
62
+ """
63
+ artists = get_all_artists(fig, include_text)
64
+
65
+ if not artists:
66
+ h = int(fig.get_figheight() * dpi)
67
+ w = int(fig.get_figwidth() * dpi)
68
+ return np.zeros((h, w), dtype=np.uint32), {}
69
+
70
+ original_props = []
71
+ color_map = {}
72
+
73
+ for i, (artist, ax_idx, artist_type) in enumerate(artists):
74
+ element_id = i + 1
75
+ r, g, b = id_to_rgb(element_id)
76
+ hex_color = f"#{r:02x}{g:02x}{b:02x}"
77
+
78
+ # Store original properties
79
+ props = {"artist": artist, "type": artist_type}
80
+ try:
81
+ if hasattr(artist, "get_color"):
82
+ props["color"] = artist.get_color()
83
+ if hasattr(artist, "get_facecolor"):
84
+ props["facecolor"] = artist.get_facecolor()
85
+ if hasattr(artist, "get_edgecolor"):
86
+ props["edgecolor"] = artist.get_edgecolor()
87
+ if hasattr(artist, "get_alpha"):
88
+ props["alpha"] = artist.get_alpha()
89
+ if hasattr(artist, "get_antialiased"):
90
+ props["antialiased"] = artist.get_antialiased()
91
+ except Exception:
92
+ pass
93
+ original_props.append(props)
94
+
95
+ # Build color map entry
96
+ label = ""
97
+ if hasattr(artist, "get_label"):
98
+ label = artist.get_label()
99
+ if label.startswith("_"):
100
+ label = f"{artist_type}_{i}"
101
+
102
+ color_map[element_id] = {
103
+ "id": element_id,
104
+ "type": artist_type,
105
+ "label": label,
106
+ "axes_index": ax_idx,
107
+ "rgb": [r, g, b],
108
+ }
109
+
110
+ # Apply ID color and disable anti-aliasing
111
+ try:
112
+ apply_id_color(artist, hex_color)
113
+ except Exception:
114
+ pass
115
+
116
+ # Make non-artist elements the reserved axes color
117
+ axes_color = HITMAP_AXES_COLOR
118
+ for ax in fig.axes:
119
+ ax.grid(False)
120
+ for spine in ax.spines.values():
121
+ spine.set_color(axes_color)
122
+ ax.set_facecolor(HITMAP_BACKGROUND_COLOR)
123
+ ax.tick_params(colors=axes_color, labelcolor=axes_color)
124
+ ax.xaxis.label.set_color(axes_color)
125
+ ax.yaxis.label.set_color(axes_color)
126
+ ax.title.set_color(axes_color)
127
+ if ax.get_legend():
128
+ ax.get_legend().set_visible(False)
129
+
130
+ fig.patch.set_facecolor(HITMAP_BACKGROUND_COLOR)
131
+
132
+ # Render
133
+ fig.canvas.draw()
134
+ img = np.array(fig.canvas.buffer_rgba())
135
+ # Convert RGB to element ID using 24-bit encoding
136
+ hitmap = (
137
+ (img[:, :, 0].astype(np.uint32) << 16)
138
+ | (img[:, :, 1].astype(np.uint32) << 8)
139
+ | img[:, :, 2].astype(np.uint32)
140
+ )
141
+
142
+ # Restore original properties
143
+ for props in original_props:
144
+ artist = props["artist"]
145
+ try:
146
+ if "color" in props and hasattr(artist, "set_color"):
147
+ artist.set_color(props["color"])
148
+ if "facecolor" in props and hasattr(artist, "set_facecolor"):
149
+ artist.set_facecolor(props["facecolor"])
150
+ if "edgecolor" in props and hasattr(artist, "set_edgecolor"):
151
+ artist.set_edgecolor(props["edgecolor"])
152
+ if "alpha" in props and hasattr(artist, "set_alpha"):
153
+ artist.set_alpha(props["alpha"])
154
+ if "antialiased" in props and hasattr(artist, "set_antialiased"):
155
+ artist.set_antialiased(props["antialiased"])
156
+ except Exception:
157
+ pass
158
+
159
+ return hitmap, color_map
160
+
161
+
162
+ def generate_hitmap_with_bbox_tight(
163
+ fig,
164
+ dpi: int = 150,
165
+ include_text: bool = False,
166
+ ) -> Tuple["Image.Image", Dict[int, Dict[str, Any]]]:
167
+ """
168
+ Generate a hitmap image with bbox_inches='tight' to match PNG output.
169
+
170
+ Parameters
171
+ ----------
172
+ fig : matplotlib.figure.Figure
173
+ The figure to generate hit map for.
174
+ dpi : int
175
+ Resolution for hit map rendering.
176
+ include_text : bool
177
+ Whether to include text elements in hit map.
178
+
179
+ Returns
180
+ -------
181
+ tuple
182
+ (hitmap_image, color_map) where:
183
+ - hitmap_image: PIL.Image.Image with RGB-encoded element IDs
184
+ - color_map: dict mapping ID to element info
185
+ """
186
+ from PIL import Image
187
+
188
+ artists = get_all_artists(fig, include_text)
189
+
190
+ if not artists:
191
+ buf = io.BytesIO()
192
+ fig.savefig(buf, format="png", dpi=dpi, bbox_inches="tight")
193
+ buf.seek(0)
194
+ img = Image.open(buf).convert("RGB")
195
+ black_img = Image.new("RGB", img.size, (0, 0, 0))
196
+ return black_img, {}
197
+
198
+ original_props = []
199
+ original_ax_props = []
200
+
201
+ # Store original axes properties
202
+ for ax in fig.axes:
203
+ ax_props = {
204
+ "ax": ax,
205
+ "facecolor": ax.get_facecolor(),
206
+ "grid_visible": (
207
+ ax.xaxis.get_gridlines()[0].get_visible()
208
+ if ax.xaxis.get_gridlines()
209
+ else False
210
+ ),
211
+ "spines": {name: spine.get_visible() for name, spine in ax.spines.items()},
212
+ "xlabel": ax.get_xlabel(),
213
+ "ylabel": ax.get_ylabel(),
214
+ "title": ax.get_title(),
215
+ "tick_params": {},
216
+ }
217
+ if ax.get_legend():
218
+ ax_props["legend_visible"] = ax.get_legend().get_visible()
219
+ original_ax_props.append(ax_props)
220
+
221
+ original_fig_facecolor = fig.patch.get_facecolor()
222
+
223
+ # Build color map
224
+ color_map = {}
225
+
226
+ for i, (artist, ax_idx, artist_type) in enumerate(artists):
227
+ element_id = i + 1
228
+ r, g, b = id_to_rgb(element_id)
229
+ hex_color = f"#{r:02x}{g:02x}{b:02x}"
230
+
231
+ # Store original properties
232
+ props = {"artist": artist, "type": artist_type}
233
+ try:
234
+ if hasattr(artist, "get_color"):
235
+ props["color"] = artist.get_color()
236
+ if hasattr(artist, "get_facecolor"):
237
+ props["facecolor"] = artist.get_facecolor()
238
+ if hasattr(artist, "get_edgecolor"):
239
+ props["edgecolor"] = artist.get_edgecolor()
240
+ if hasattr(artist, "get_alpha"):
241
+ props["alpha"] = artist.get_alpha()
242
+ if hasattr(artist, "get_antialiased"):
243
+ props["antialiased"] = artist.get_antialiased()
244
+ except Exception:
245
+ pass
246
+ original_props.append(props)
247
+
248
+ # Build color map entry
249
+ label = ""
250
+ if hasattr(artist, "get_label"):
251
+ label = artist.get_label()
252
+ if label.startswith("_"):
253
+ label = f"{artist_type}_{i}"
254
+
255
+ color_map[element_id] = {
256
+ "id": element_id,
257
+ "type": artist_type,
258
+ "label": label,
259
+ "axes_index": ax_idx,
260
+ "rgb": [r, g, b],
261
+ }
262
+
263
+ # Apply ID color
264
+ try:
265
+ apply_id_color(artist, hex_color)
266
+ except Exception:
267
+ pass
268
+
269
+ # Make non-artist elements reserved axes color
270
+ axes_color = HITMAP_AXES_COLOR
271
+ for ax in fig.axes:
272
+ ax.grid(False)
273
+ for spine in ax.spines.values():
274
+ spine.set_color(axes_color)
275
+ ax.set_facecolor(HITMAP_BACKGROUND_COLOR)
276
+ ax.tick_params(colors=axes_color, labelcolor=axes_color)
277
+ ax.xaxis.label.set_color(axes_color)
278
+ ax.yaxis.label.set_color(axes_color)
279
+ ax.title.set_color(axes_color)
280
+ if ax.get_legend():
281
+ ax.get_legend().set_visible(False)
282
+
283
+ fig.patch.set_facecolor(HITMAP_BACKGROUND_COLOR)
284
+
285
+ # Save hitmap with bbox_inches='tight'
286
+ buf = io.BytesIO()
287
+ fig.savefig(
288
+ buf,
289
+ format="png",
290
+ dpi=dpi,
291
+ bbox_inches="tight",
292
+ facecolor=HITMAP_BACKGROUND_COLOR,
293
+ )
294
+ buf.seek(0)
295
+ hitmap_img = Image.open(buf).convert("RGB")
296
+
297
+ # Restore original properties
298
+ for props in original_props:
299
+ artist = props["artist"]
300
+ try:
301
+ if "color" in props and hasattr(artist, "set_color"):
302
+ artist.set_color(props["color"])
303
+ if "facecolor" in props and hasattr(artist, "set_facecolor"):
304
+ artist.set_facecolor(props["facecolor"])
305
+ if "edgecolor" in props and hasattr(artist, "set_edgecolor"):
306
+ artist.set_edgecolor(props["edgecolor"])
307
+ if "alpha" in props and hasattr(artist, "set_alpha"):
308
+ artist.set_alpha(props["alpha"])
309
+ if "antialiased" in props and hasattr(artist, "set_antialiased"):
310
+ artist.set_antialiased(props["antialiased"])
311
+ except Exception:
312
+ pass
313
+
314
+ # Restore axes properties
315
+ for ax_props in original_ax_props:
316
+ ax = ax_props["ax"]
317
+ try:
318
+ ax.set_facecolor(ax_props["facecolor"])
319
+ for name, visible in ax_props["spines"].items():
320
+ ax.spines[name].set_visible(visible)
321
+ ax.set_xlabel(ax_props["xlabel"])
322
+ ax.set_ylabel(ax_props["ylabel"])
323
+ ax.set_title(ax_props["title"])
324
+ if "legend_visible" in ax_props and ax.get_legend():
325
+ ax.get_legend().set_visible(ax_props["legend_visible"])
326
+ except Exception:
327
+ pass
328
+
329
+ fig.patch.set_facecolor(original_fig_facecolor)
330
+
331
+ return hitmap_img, color_map
332
+
333
+
334
+ # EOF
@@ -0,0 +1,357 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: "2026-01-24 (ywatanabe)"
3
+ # File: /home/ywatanabe/proj/scitex-python/src/scitex/plt/utils/_hitmap/_path_extraction.py
4
+
5
+ """
6
+ Path data and selectable regions extraction for hitmap generation.
7
+
8
+ This module provides functions to extract path/geometry data and selectable
9
+ regions for client-side hit testing.
10
+ """
11
+
12
+ import warnings
13
+ from typing import Any, Dict, List, Optional
14
+
15
+ import numpy as np
16
+
17
+ from ._artist_extraction import get_all_artists
18
+ from ._constants import to_native
19
+
20
+ __all__ = [
21
+ "extract_path_data",
22
+ "extract_selectable_regions",
23
+ ]
24
+
25
+
26
+ def extract_path_data(
27
+ fig,
28
+ include_text: bool = False,
29
+ ) -> Dict[str, Any]:
30
+ """
31
+ Extract path/geometry data for client-side hit testing.
32
+
33
+ Parameters
34
+ ----------
35
+ fig : matplotlib.figure.Figure
36
+ The figure to extract data from.
37
+ include_text : bool
38
+ Whether to include text elements.
39
+
40
+ Returns
41
+ -------
42
+ dict
43
+ Exported data structure with figure info and artist geometries.
44
+
45
+ Notes
46
+ -----
47
+ Performance: ~192ms extraction, ~0.01ms client-side queries
48
+ """
49
+ with warnings.catch_warnings():
50
+ warnings.filterwarnings("ignore", message=".*tight_layout.*")
51
+ fig.canvas.draw()
52
+
53
+ artists = get_all_artists(fig, include_text)
54
+
55
+ dpi = fig.dpi
56
+ fig_width_px = int(fig.get_figwidth() * dpi)
57
+ fig_height_px = int(fig.get_figheight() * dpi)
58
+
59
+ export = {
60
+ "figure": {
61
+ "width_px": fig_width_px,
62
+ "height_px": fig_height_px,
63
+ "dpi": dpi,
64
+ },
65
+ "axes": [],
66
+ "artists": [],
67
+ }
68
+
69
+ # Export axes info
70
+ for ax in fig.axes:
71
+ bbox = ax.get_position()
72
+ export["axes"].append(
73
+ {
74
+ "xlim": list(ax.get_xlim()),
75
+ "ylim": list(ax.get_ylim()),
76
+ "bbox_norm": {
77
+ "x0": bbox.x0,
78
+ "y0": bbox.y0,
79
+ "x1": bbox.x1,
80
+ "y1": bbox.y1,
81
+ },
82
+ "bbox_px": {
83
+ "x0": int(bbox.x0 * fig_width_px),
84
+ "y0": int((1 - bbox.y1) * fig_height_px),
85
+ "x1": int(bbox.x1 * fig_width_px),
86
+ "y1": int((1 - bbox.y0) * fig_height_px),
87
+ },
88
+ }
89
+ )
90
+
91
+ # Export artist geometries
92
+ renderer = fig.canvas.get_renderer()
93
+
94
+ for i, (artist, ax_idx, artist_type) in enumerate(artists):
95
+ artist_data = {
96
+ "id": i,
97
+ "type": artist_type,
98
+ "axes_index": ax_idx,
99
+ "label": "",
100
+ }
101
+
102
+ # Get label
103
+ if hasattr(artist, "get_label"):
104
+ label = artist.get_label()
105
+ artist_data["label"] = (
106
+ label if not label.startswith("_") else f"{artist_type}_{i}"
107
+ )
108
+
109
+ # Get bounding box
110
+ try:
111
+ bbox = artist.get_window_extent(renderer)
112
+ artist_data["bbox_px"] = {
113
+ "x0": float(bbox.x0),
114
+ "y0": float(fig_height_px - bbox.y1),
115
+ "x1": float(bbox.x1),
116
+ "y1": float(fig_height_px - bbox.y0),
117
+ }
118
+ except Exception:
119
+ artist_data["bbox_px"] = None
120
+
121
+ # Extract type-specific geometry
122
+ try:
123
+ if artist_type == "line" and hasattr(artist, "get_xydata"):
124
+ xy = artist.get_xydata()
125
+ transform = artist.get_transform()
126
+ xy_px = transform.transform(xy)
127
+ xy_px[:, 1] = fig_height_px - xy_px[:, 1]
128
+ if len(xy_px) > 100:
129
+ indices = np.linspace(0, len(xy_px) - 1, 100, dtype=int)
130
+ xy_px = xy_px[indices]
131
+ artist_data["path_px"] = xy_px.tolist()
132
+ artist_data["linewidth"] = artist.get_linewidth()
133
+
134
+ elif artist_type == "scatter" and hasattr(artist, "get_offsets"):
135
+ offsets = artist.get_offsets()
136
+ transform = artist.get_transform()
137
+ offsets_px = transform.transform(offsets)
138
+ offsets_px[:, 1] = fig_height_px - offsets_px[:, 1]
139
+ artist_data["points_px"] = offsets_px.tolist()
140
+ sizes = artist.get_sizes()
141
+ artist_data["sizes"] = sizes.tolist() if len(sizes) > 0 else [36]
142
+
143
+ elif artist_type == "fill" and hasattr(artist, "get_paths"):
144
+ paths = artist.get_paths()
145
+ if paths:
146
+ transform = artist.get_transform()
147
+ vertices = paths[0].vertices
148
+ vertices_px = transform.transform(vertices)
149
+ vertices_px[:, 1] = fig_height_px - vertices_px[:, 1]
150
+ if len(vertices_px) > 100:
151
+ indices = np.linspace(0, len(vertices_px) - 1, 100, dtype=int)
152
+ vertices_px = vertices_px[indices]
153
+ artist_data["polygon_px"] = vertices_px.tolist()
154
+
155
+ elif artist_type == "bar" and hasattr(artist, "patches"):
156
+ bars = []
157
+ for patch in artist.patches:
158
+ x_data = patch.get_x()
159
+ y_data = patch.get_y()
160
+ w_data = patch.get_width()
161
+ h_data = patch.get_height()
162
+ bars.append(
163
+ {
164
+ "x": x_data,
165
+ "y": y_data,
166
+ "width": w_data,
167
+ "height": h_data,
168
+ }
169
+ )
170
+ artist_data["bars_data"] = bars
171
+
172
+ elif artist_type == "rectangle":
173
+ artist_data["rectangle"] = {
174
+ "x": artist.get_x(),
175
+ "y": artist.get_y(),
176
+ "width": artist.get_width(),
177
+ "height": artist.get_height(),
178
+ }
179
+
180
+ except Exception as e:
181
+ artist_data["error"] = str(e)
182
+
183
+ export["artists"].append(artist_data)
184
+
185
+ return to_native(export)
186
+
187
+
188
+ def extract_selectable_regions(fig) -> Dict[str, Any]:
189
+ """
190
+ Extract bounding boxes for axis/annotation elements (non-data elements).
191
+
192
+ Parameters
193
+ ----------
194
+ fig : matplotlib.figure.Figure
195
+ The figure to extract regions from.
196
+
197
+ Returns
198
+ -------
199
+ dict
200
+ Dictionary with selectable regions.
201
+ """
202
+ with warnings.catch_warnings():
203
+ warnings.filterwarnings("ignore", message=".*tight_layout.*")
204
+ fig.canvas.draw()
205
+
206
+ dpi = fig.dpi
207
+ fig_width_px = int(fig.get_figwidth() * dpi)
208
+ fig_height_px = int(fig.get_figheight() * dpi)
209
+
210
+ renderer = fig.canvas.get_renderer()
211
+
212
+ def get_bbox_px(artist) -> Optional[List[float]]:
213
+ """Get bounding box in pixels (y-flipped for image coordinates)."""
214
+ try:
215
+ bbox = artist.get_window_extent(renderer)
216
+ if bbox.width > 0 and bbox.height > 0:
217
+ return [
218
+ float(bbox.x0),
219
+ float(fig_height_px - bbox.y1),
220
+ float(bbox.x1),
221
+ float(fig_height_px - bbox.y0),
222
+ ]
223
+ except Exception:
224
+ pass
225
+ return None
226
+
227
+ def get_text_info(text_artist) -> Optional[Dict[str, Any]]:
228
+ """Extract text element info with bounding box."""
229
+ if text_artist is None:
230
+ return None
231
+ text = text_artist.get_text()
232
+ if not text or not text.strip():
233
+ return None
234
+ bbox = get_bbox_px(text_artist)
235
+ if bbox is None:
236
+ return None
237
+ return {
238
+ "bbox_px": bbox,
239
+ "text": text,
240
+ "fontsize": text_artist.get_fontsize(),
241
+ "color": text_artist.get_color(),
242
+ }
243
+
244
+ regions = {"axes": []}
245
+
246
+ for ax_idx, ax in enumerate(fig.axes):
247
+ ax_region = {"index": ax_idx}
248
+
249
+ # Title
250
+ title_info = get_text_info(ax.title)
251
+ if title_info:
252
+ ax_region["title"] = title_info
253
+
254
+ # X label
255
+ xlabel_info = get_text_info(ax.xaxis.label)
256
+ if xlabel_info:
257
+ ax_region["xlabel"] = xlabel_info
258
+
259
+ # Y label
260
+ ylabel_info = get_text_info(ax.yaxis.label)
261
+ if ylabel_info:
262
+ ax_region["ylabel"] = ylabel_info
263
+
264
+ # X axis elements
265
+ xaxis_info = {"spine": None, "ticks": [], "ticklabels": []}
266
+
267
+ if ax.spines["bottom"].get_visible():
268
+ spine_bbox = get_bbox_px(ax.spines["bottom"])
269
+ if spine_bbox:
270
+ xaxis_info["spine"] = {"bbox_px": spine_bbox}
271
+
272
+ for tick in ax.xaxis.get_major_ticks():
273
+ if tick.tick1line.get_visible():
274
+ tick_bbox = get_bbox_px(tick.tick1line)
275
+ if tick_bbox:
276
+ xaxis_info["ticks"].append(
277
+ {
278
+ "bbox_px": tick_bbox,
279
+ "position": (
280
+ float(tick.get_loc())
281
+ if hasattr(tick, "get_loc")
282
+ else None
283
+ ),
284
+ }
285
+ )
286
+
287
+ if tick.label1.get_visible():
288
+ label_info = get_text_info(tick.label1)
289
+ if label_info:
290
+ xaxis_info["ticklabels"].append(label_info)
291
+
292
+ if xaxis_info["spine"] or xaxis_info["ticks"] or xaxis_info["ticklabels"]:
293
+ ax_region["xaxis"] = xaxis_info
294
+
295
+ # Y axis elements
296
+ yaxis_info = {"spine": None, "ticks": [], "ticklabels": []}
297
+
298
+ if ax.spines["left"].get_visible():
299
+ spine_bbox = get_bbox_px(ax.spines["left"])
300
+ if spine_bbox:
301
+ yaxis_info["spine"] = {"bbox_px": spine_bbox}
302
+
303
+ for tick in ax.yaxis.get_major_ticks():
304
+ if tick.tick1line.get_visible():
305
+ tick_bbox = get_bbox_px(tick.tick1line)
306
+ if tick_bbox:
307
+ yaxis_info["ticks"].append(
308
+ {
309
+ "bbox_px": tick_bbox,
310
+ "position": (
311
+ float(tick.get_loc())
312
+ if hasattr(tick, "get_loc")
313
+ else None
314
+ ),
315
+ }
316
+ )
317
+
318
+ if tick.label1.get_visible():
319
+ label_info = get_text_info(tick.label1)
320
+ if label_info:
321
+ yaxis_info["ticklabels"].append(label_info)
322
+
323
+ if yaxis_info["spine"] or yaxis_info["ticks"] or yaxis_info["ticklabels"]:
324
+ ax_region["yaxis"] = yaxis_info
325
+
326
+ # Legend
327
+ legend = ax.get_legend()
328
+ if legend and legend.get_visible():
329
+ legend_info = {"bbox_px": None, "entries": []}
330
+
331
+ legend_bbox = get_bbox_px(legend)
332
+ if legend_bbox:
333
+ legend_info["bbox_px"] = legend_bbox
334
+
335
+ for text in legend.get_texts():
336
+ entry_info = get_text_info(text)
337
+ if entry_info:
338
+ legend_info["entries"].append(entry_info)
339
+
340
+ try:
341
+ handles = legend.legendHandles
342
+ for i, handle in enumerate(handles):
343
+ handle_bbox = get_bbox_px(handle)
344
+ if handle_bbox and i < len(legend_info["entries"]):
345
+ legend_info["entries"][i]["handle_bbox_px"] = handle_bbox
346
+ except Exception:
347
+ pass
348
+
349
+ if legend_info["bbox_px"] or legend_info["entries"]:
350
+ ax_region["legend"] = legend_info
351
+
352
+ regions["axes"].append(ax_region)
353
+
354
+ return to_native(regions)
355
+
356
+
357
+ # EOF