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
@@ -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