scitex 2.14.0__py3-none-any.whl → 2.15.3__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 (264) 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 +27 -0
  16. scitex/_mcp_tools/template.py +24 -0
  17. scitex/_mcp_tools/writer.py +17 -210
  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 +160 -41
  68. scitex/cli/capture.py +133 -20
  69. scitex/cli/introspect.py +488 -0
  70. scitex/cli/main.py +200 -109
  71. scitex/cli/mcp.py +60 -34
  72. scitex/cli/plt.py +414 -0
  73. scitex/cli/repro.py +15 -8
  74. scitex/cli/resource.py +15 -8
  75. scitex/cli/scholar/__init__.py +154 -8
  76. scitex/cli/scholar/_crossref_scitex.py +296 -0
  77. scitex/cli/scholar/_fetch.py +25 -3
  78. scitex/cli/social.py +355 -0
  79. scitex/cli/stats.py +136 -11
  80. scitex/cli/template.py +129 -12
  81. scitex/cli/tex.py +15 -8
  82. scitex/cli/writer.py +49 -299
  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} +48 -56
  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/security/README.md +3 -3
  178. scitex/session/README.md +1 -1
  179. scitex/session/__init__.py +26 -7
  180. scitex/session/_decorator.py +1 -1
  181. scitex/sh/README.md +1 -1
  182. scitex/sh/__init__.py +7 -4
  183. scitex/social/__init__.py +155 -0
  184. scitex/social/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
  185. scitex/stats/_mcp/_handlers/__init__.py +31 -0
  186. scitex/stats/_mcp/_handlers/_corrections.py +113 -0
  187. scitex/stats/_mcp/_handlers/_descriptive.py +78 -0
  188. scitex/stats/_mcp/_handlers/_effect_size.py +106 -0
  189. scitex/stats/_mcp/_handlers/_format.py +94 -0
  190. scitex/stats/_mcp/_handlers/_normality.py +110 -0
  191. scitex/stats/_mcp/_handlers/_posthoc.py +224 -0
  192. scitex/stats/_mcp/_handlers/_power.py +247 -0
  193. scitex/stats/_mcp/_handlers/_recommend.py +102 -0
  194. scitex/stats/_mcp/_handlers/_run_test.py +279 -0
  195. scitex/stats/_mcp/_handlers/_stars.py +48 -0
  196. scitex/stats/_mcp/handlers.py +19 -1171
  197. scitex/stats/auto/_stat_style.py +175 -0
  198. scitex/stats/auto/_style_definitions.py +411 -0
  199. scitex/stats/auto/_styles.py +22 -620
  200. scitex/stats/descriptive/__init__.py +11 -8
  201. scitex/stats/descriptive/_ci.py +39 -0
  202. scitex/stats/power/_power.py +15 -4
  203. scitex/str/__init__.py +2 -1
  204. scitex/str/_title_case.py +63 -0
  205. scitex/template/README.md +1 -1
  206. scitex/template/__init__.py +25 -10
  207. scitex/template/_code_templates.py +147 -0
  208. scitex/template/_mcp/handlers.py +81 -0
  209. scitex/template/_mcp/tool_schemas.py +55 -0
  210. scitex/template/_templates/__init__.py +51 -0
  211. scitex/template/_templates/audio.py +233 -0
  212. scitex/template/_templates/canvas.py +312 -0
  213. scitex/template/_templates/capture.py +268 -0
  214. scitex/template/_templates/config.py +43 -0
  215. scitex/template/_templates/diagram.py +294 -0
  216. scitex/template/_templates/io.py +107 -0
  217. scitex/template/_templates/module.py +53 -0
  218. scitex/template/_templates/plt.py +202 -0
  219. scitex/template/_templates/scholar.py +267 -0
  220. scitex/template/_templates/session.py +130 -0
  221. scitex/template/_templates/session_minimal.py +43 -0
  222. scitex/template/_templates/session_plot.py +67 -0
  223. scitex/template/_templates/session_stats.py +77 -0
  224. scitex/template/_templates/stats.py +323 -0
  225. scitex/template/_templates/writer.py +296 -0
  226. scitex/template/clone_writer_directory.py +5 -5
  227. scitex/ui/_backends/_email.py +10 -2
  228. scitex/ui/_backends/_webhook.py +5 -1
  229. scitex/web/_search_pubmed.py +10 -6
  230. scitex/writer/README.md +1 -1
  231. scitex/writer/__init__.py +43 -34
  232. scitex/writer/_mcp/handlers.py +11 -744
  233. scitex/writer/_mcp/tool_schemas.py +5 -335
  234. scitex-2.15.3.dist-info/METADATA +667 -0
  235. {scitex-2.14.0.dist-info → scitex-2.15.3.dist-info}/RECORD +241 -120
  236. scitex/canvas/editor/flask_editor/templates/_scripts.py +0 -4933
  237. scitex/canvas/editor/flask_editor/templates/_styles.py +0 -1658
  238. scitex/diagram/_compile.py +0 -312
  239. scitex/diagram/_diagram.py +0 -355
  240. scitex/diagram/_mcp/__init__.py +0 -4
  241. scitex/diagram/_mcp/handlers.py +0 -400
  242. scitex/diagram/_mcp/tool_schemas.py +0 -157
  243. scitex/diagram/_presets.py +0 -173
  244. scitex/diagram/_schema.py +0 -182
  245. scitex/diagram/_split.py +0 -278
  246. scitex/gen/_ci.py +0 -12
  247. scitex/gen/_title_case.py +0 -89
  248. scitex/plt/_mcp/__init__.py +0 -4
  249. scitex/plt/_mcp/_handlers_annotation.py +0 -102
  250. scitex/plt/_mcp/_handlers_figure.py +0 -195
  251. scitex/plt/_mcp/_handlers_plot.py +0 -252
  252. scitex/plt/_mcp/_handlers_style.py +0 -219
  253. scitex/plt/_mcp/handlers.py +0 -74
  254. scitex/plt/_mcp/tool_schemas.py +0 -497
  255. scitex/plt/mcp_server.py +0 -231
  256. scitex/scholar/examples/SUGGESTIONS.md +0 -865
  257. scitex/scholar/examples/dev.py +0 -38
  258. scitex-2.14.0.dist-info/METADATA +0 -1238
  259. /scitex/{gen → context}/_detect_environment.py +0 -0
  260. /scitex/{gen → context}/_get_notebook_path.py +0 -0
  261. /scitex/{gen/_shell.py → sh/_shell_legacy.py} +0 -0
  262. {scitex-2.14.0.dist-info → scitex-2.15.3.dist-info}/WHEEL +0 -0
  263. {scitex-2.14.0.dist-info → scitex-2.15.3.dist-info}/entry_points.txt +0 -0
  264. {scitex-2.14.0.dist-info → scitex-2.15.3.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