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,57 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: "2026-01-24 (ywatanabe)"
3
+ # File: /home/ywatanabe/proj/scitex-python/src/scitex/plt/utils/_metadata/_artists/_extract.py
4
+
5
+ """
6
+ Main artist extraction function.
7
+
8
+ Orchestrates extraction of all artist types from matplotlib axes.
9
+ """
10
+
11
+ from typing import List
12
+
13
+ from ._base import create_extraction_context
14
+ from ._collections import extract_collections
15
+ from ._images import extract_images
16
+ from ._lines import extract_lines
17
+ from ._patches import extract_patches
18
+ from ._text import extract_text
19
+
20
+
21
+ def _extract_artists(ax) -> List[dict]:
22
+ """
23
+ Extract artist information including properties and CSV column mapping.
24
+
25
+ Uses matplotlib terminology: each drawable element is an Artist.
26
+ Only includes artists that were explicitly created via scitex tracking,
27
+ not internal artists created by matplotlib functions.
28
+
29
+ Parameters
30
+ ----------
31
+ ax : matplotlib.axes.Axes
32
+ The axes to extract artists from
33
+
34
+ Returns
35
+ -------
36
+ list
37
+ List of artist dictionaries with:
38
+ - id: unique identifier
39
+ - artist_class: matplotlib class name (Line2D, PathCollection, etc.)
40
+ - label: legend label
41
+ - style: color, linestyle, linewidth, etc.
42
+ - data_ref: CSV column mapping (matches columns_actual exactly)
43
+ """
44
+ ctx = create_extraction_context(ax)
45
+ artists = []
46
+
47
+ # Extract different artist types
48
+ artists.extend(extract_lines(ctx))
49
+ artists.extend(extract_patches(ctx))
50
+ artists.extend(extract_collections(ctx))
51
+ artists.extend(extract_images(ctx))
52
+ artists.extend(extract_text(ctx))
53
+
54
+ return artists
55
+
56
+
57
+ # EOF
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: "2026-01-24 (ywatanabe)"
3
+ # File: /home/ywatanabe/proj/scitex-python/src/scitex/plt/utils/_metadata/_artists/_images.py
4
+
5
+ """
6
+ Image artist extraction.
7
+
8
+ Handles AxesImage (imshow, matshow) extraction.
9
+ """
10
+
11
+ from typing import List
12
+
13
+ from ._base import ExtractionContext
14
+
15
+
16
+ def extract_images(ctx: ExtractionContext) -> List[dict]:
17
+ """Extract image artists from axes."""
18
+ artists = []
19
+
20
+ for i, img in enumerate(ctx.mpl_ax.images):
21
+ artist = _extract_image(ctx, i, img)
22
+ if artist:
23
+ artists.append(artist)
24
+
25
+ return artists
26
+
27
+
28
+ def _extract_image(ctx: ExtractionContext, index: int, img) -> dict:
29
+ """Extract AxesImage artist."""
30
+ artist = {}
31
+ img_type = type(img).__name__
32
+
33
+ scitex_id = getattr(img, "_scitex_id", None)
34
+ label = img.get_label() if hasattr(img, "get_label") else ""
35
+
36
+ if scitex_id:
37
+ artist["id"] = scitex_id
38
+ elif label and not label.startswith("_"):
39
+ artist["id"] = label
40
+ else:
41
+ artist["id"] = f"image_{index}"
42
+
43
+ # Semantic layer
44
+ artist["mark"] = "image"
45
+ artist["role"] = "image"
46
+
47
+ artist["legend_included"] = False
48
+ artist["zorder"] = img.get_zorder()
49
+
50
+ # Backend layer
51
+ backend = {
52
+ "name": "matplotlib",
53
+ "artist_class": img_type,
54
+ "props": {},
55
+ }
56
+
57
+ try:
58
+ cmap = img.get_cmap()
59
+ if cmap:
60
+ backend["props"]["cmap"] = cmap.name
61
+ except (ValueError, TypeError, AttributeError):
62
+ pass
63
+
64
+ try:
65
+ backend["props"]["vmin"] = float(img.norm.vmin) if img.norm else None
66
+ backend["props"]["vmax"] = float(img.norm.vmax) if img.norm else None
67
+ except (ValueError, TypeError, AttributeError):
68
+ pass
69
+
70
+ try:
71
+ backend["props"]["interpolation"] = img.get_interpolation()
72
+ except (ValueError, TypeError, AttributeError):
73
+ pass
74
+
75
+ artist["backend"] = backend
76
+
77
+ return artist
78
+
79
+
80
+ # EOF
@@ -0,0 +1,261 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: "2026-01-24 (ywatanabe)"
3
+ # File: /home/ywatanabe/proj/scitex-python/src/scitex/plt/utils/_metadata/_artists/_lines.py
4
+
5
+ """
6
+ Line2D artist extraction.
7
+
8
+ Handles extraction of line plots, including special handling for
9
+ boxplot, violin, and stem semantic components.
10
+ """
11
+
12
+ from typing import List, Optional, Tuple
13
+
14
+ from ._base import ExtractionContext, color_to_hex
15
+
16
+
17
+ def extract_lines(ctx: ExtractionContext) -> List[dict]:
18
+ """Extract Line2D artists from axes."""
19
+ from .._csv import _get_csv_column_names
20
+
21
+ artists = []
22
+
23
+ for i, line in enumerate(ctx.mpl_ax.lines):
24
+ scitex_id = getattr(line, "_scitex_id", None)
25
+ label = line.get_label()
26
+
27
+ # Determine semantic type for special plot types
28
+ semantic_type, semantic_id, box_idx = _get_line_semantic_info(
29
+ ctx, i, line, scitex_id, label
30
+ )
31
+
32
+ # Skip internal artists for certain plot types
33
+ if _should_skip_line(ctx, scitex_id, label, semantic_type):
34
+ continue
35
+
36
+ artist = _build_line_artist(
37
+ ctx, i, line, scitex_id, label, semantic_type, semantic_id, box_idx
38
+ )
39
+
40
+ # Add data_ref for non-semantic lines
41
+ if not semantic_type:
42
+ trace_id = _get_trace_id_for_line(ctx, i, scitex_id, artist.get("id"))
43
+ artist["data_ref"] = _get_csv_column_names(trace_id, ctx.ax_row, ctx.ax_col)
44
+ elif ctx.is_stem and scitex_id:
45
+ artist["data_ref"] = _get_csv_column_names(
46
+ scitex_id, ctx.ax_row, ctx.ax_col
47
+ )
48
+ if semantic_type == "stem_baseline":
49
+ artist["derived"] = True
50
+ artist["data_ref"]["derived_from"] = "y=0"
51
+
52
+ # Add boxplot statistics
53
+ if (
54
+ semantic_type == "boxplot_median"
55
+ and box_idx is not None
56
+ and box_idx < len(ctx.boxplot_stats)
57
+ ):
58
+ artist["stats"] = ctx.boxplot_stats[box_idx]
59
+
60
+ artists.append(artist)
61
+
62
+ return artists
63
+
64
+
65
+ def _get_line_semantic_info(
66
+ ctx: ExtractionContext,
67
+ index: int,
68
+ line,
69
+ scitex_id: Optional[str],
70
+ label: str,
71
+ ) -> Tuple[Optional[str], Optional[str], Optional[int]]:
72
+ """Get semantic type info for a line."""
73
+ semantic_type = None
74
+ semantic_id = None
75
+ box_idx = None
76
+
77
+ # Stem detection
78
+ if ctx.is_stem:
79
+ semantic_type, semantic_id = _detect_stem_semantic(line, index)
80
+
81
+ # Boxplot detection for unlabeled lines
82
+ if (
83
+ ctx.skip_unlabeled
84
+ and not scitex_id
85
+ and label.startswith("_")
86
+ and ctx.is_boxplot
87
+ and ctx.num_boxes > 0
88
+ ):
89
+ semantic_type, semantic_id, box_idx = _detect_boxplot_semantic(
90
+ ctx.num_boxes, index
91
+ )
92
+ elif (
93
+ ctx.skip_unlabeled and not scitex_id and label.startswith("_") and ctx.is_violin
94
+ ):
95
+ semantic_type = "violin_component"
96
+ semantic_id = f"violin_line_{index}"
97
+
98
+ return semantic_type, semantic_id, box_idx
99
+
100
+
101
+ def _detect_stem_semantic(line, index: int) -> Tuple[str, str]:
102
+ """Detect stem plot semantic type."""
103
+ marker = line.get_marker()
104
+ linestyle = line.get_linestyle()
105
+
106
+ if marker and marker != "None" and linestyle == "None":
107
+ return "stem_marker", "stem_markers"
108
+ elif linestyle and linestyle != "None":
109
+ ydata = line.get_ydata()
110
+ if len(ydata) >= 2 and len(set(ydata)) == 1:
111
+ return "stem_baseline", "stem_baseline"
112
+ else:
113
+ return "stem_stem", "stem_lines"
114
+ else:
115
+ return "stem_component", f"stem_{index}"
116
+
117
+
118
+ def _detect_boxplot_semantic(num_boxes: int, index: int) -> Tuple[str, str, int]:
119
+ """Detect boxplot semantic type based on line index."""
120
+ total_whiskers = 2 * num_boxes
121
+ total_caps = 2 * num_boxes
122
+ total_medians = num_boxes
123
+
124
+ if index < total_whiskers:
125
+ box_idx = index // 2
126
+ whisker_idx = index % 2
127
+ return "boxplot_whisker", f"box_{box_idx}_whisker_{whisker_idx}", box_idx
128
+ elif index < total_whiskers + total_caps:
129
+ cap_i = index - total_whiskers
130
+ box_idx = cap_i // 2
131
+ cap_idx = cap_i % 2
132
+ return "boxplot_cap", f"box_{box_idx}_cap_{cap_idx}", box_idx
133
+ elif index < total_whiskers + total_caps + total_medians:
134
+ box_idx = index - total_whiskers - total_caps
135
+ return "boxplot_median", f"box_{box_idx}_median", box_idx
136
+ else:
137
+ flier_idx = index - total_whiskers - total_caps - total_medians
138
+ box_idx = flier_idx if flier_idx < num_boxes else num_boxes - 1
139
+ return "boxplot_flier", f"box_{box_idx}_flier", box_idx
140
+
141
+
142
+ def _should_skip_line(
143
+ ctx: ExtractionContext,
144
+ scitex_id: Optional[str],
145
+ label: str,
146
+ semantic_type: Optional[str],
147
+ ) -> bool:
148
+ """Check if line should be skipped."""
149
+ if ctx.skip_unlabeled and not scitex_id and label.startswith("_"):
150
+ # Allow boxplot, violin, stem semantic types
151
+ if ctx.is_boxplot or ctx.is_violin or ctx.is_stem:
152
+ return False
153
+ return True
154
+ return False
155
+
156
+
157
+ def _build_line_artist(
158
+ ctx: ExtractionContext,
159
+ index: int,
160
+ line,
161
+ scitex_id: Optional[str],
162
+ label: str,
163
+ semantic_type: Optional[str],
164
+ semantic_id: Optional[str],
165
+ box_idx: Optional[int],
166
+ ) -> dict:
167
+ """Build artist dict for a line."""
168
+ artist = {}
169
+
170
+ # ID assignment
171
+ if semantic_id and ctx.is_stem:
172
+ artist["id"] = semantic_id
173
+ if scitex_id:
174
+ artist["group_id"] = scitex_id
175
+ elif scitex_id:
176
+ artist["id"] = scitex_id
177
+ elif semantic_id:
178
+ artist["id"] = semantic_id
179
+ elif not label.startswith("_"):
180
+ artist["id"] = label
181
+ else:
182
+ artist["id"] = f"line_{index}"
183
+
184
+ # Semantic layer
185
+ artist["mark"] = "line"
186
+ if semantic_type:
187
+ artist["role"] = semantic_type
188
+
189
+ # Legend
190
+ if not label.startswith("_"):
191
+ artist["label"] = label
192
+ artist["legend_included"] = True
193
+ else:
194
+ artist["legend_included"] = False
195
+
196
+ artist["zorder"] = line.get_zorder()
197
+
198
+ # Backend layer
199
+ backend = {
200
+ "name": "matplotlib",
201
+ "artist_class": type(line).__name__,
202
+ "props": {},
203
+ }
204
+
205
+ color_hex = color_to_hex(line.get_color())
206
+ if color_hex:
207
+ backend["props"]["color"] = color_hex
208
+
209
+ backend["props"]["linestyle"] = line.get_linestyle()
210
+ backend["props"]["linewidth_pt"] = line.get_linewidth()
211
+
212
+ marker = line.get_marker()
213
+ if marker and marker != "None" and marker != "none":
214
+ backend["props"]["marker"] = marker
215
+ backend["props"]["markersize_pt"] = line.get_markersize()
216
+ else:
217
+ backend["props"]["marker"] = None
218
+
219
+ artist["backend"] = backend
220
+
221
+ return artist
222
+
223
+
224
+ def _get_trace_id_for_line(
225
+ ctx: ExtractionContext,
226
+ index: int,
227
+ scitex_id: Optional[str],
228
+ artist_id: str,
229
+ ) -> str:
230
+ """Get trace ID for data_ref."""
231
+ if scitex_id:
232
+ return scitex_id
233
+
234
+ # Try to find from history
235
+ if hasattr(ctx.ax_for_detection, "history"):
236
+ plot_records = []
237
+ for record_id, record in ctx.ax_for_detection.history.items():
238
+ if isinstance(record, tuple) and len(record) >= 2:
239
+ if record[1] == "plot":
240
+ tracking_id = record[0]
241
+ if tracking_id.startswith("ax_"):
242
+ parts = tracking_id.split("_")
243
+ if len(parts) >= 4:
244
+ trace_id = "_".join(parts[3:])
245
+ else:
246
+ trace_id = parts[-1]
247
+ elif tracking_id.startswith("plot_"):
248
+ trace_id = (
249
+ tracking_id[5:] if len(tracking_id) > 5 else str(index)
250
+ )
251
+ else:
252
+ trace_id = tracking_id
253
+ plot_records.append(trace_id)
254
+
255
+ if plot_records and index < len(plot_records):
256
+ return plot_records[index]
257
+
258
+ return artist_id
259
+
260
+
261
+ # EOF
@@ -0,0 +1,247 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: "2026-01-24 (ywatanabe)"
3
+ # File: /home/ywatanabe/proj/scitex-python/src/scitex/plt/utils/_metadata/_artists/_patches.py
4
+
5
+ """
6
+ Patch artist extraction.
7
+
8
+ Handles Rectangle (bar, hist), Wedge (pie), and Polygon patches.
9
+ """
10
+
11
+ from typing import List, Optional
12
+
13
+ from ._base import ExtractionContext, color_to_hex
14
+
15
+
16
+ def extract_patches(ctx: ExtractionContext) -> List[dict]:
17
+ """Extract patch artists from axes."""
18
+ artists = []
19
+
20
+ # Collect rectangles
21
+ rectangles = []
22
+ for i, patch in enumerate(ctx.mpl_ax.patches):
23
+ patch_type = type(patch).__name__
24
+ if patch_type == "Rectangle":
25
+ rectangles.append((i, patch))
26
+ elif patch_type == "Wedge":
27
+ artist = _extract_wedge(ctx, i, patch)
28
+ if artist:
29
+ artists.append(artist)
30
+ elif "Poly" in patch_type:
31
+ artist = _extract_polygon(ctx, i, patch)
32
+ if artist:
33
+ artists.append(artist)
34
+
35
+ # Extract rectangles (bar/hist)
36
+ if rectangles:
37
+ bar_artists = _extract_rectangles(ctx, rectangles)
38
+ artists.extend(bar_artists)
39
+
40
+ return artists
41
+
42
+
43
+ def _extract_rectangles(ctx: ExtractionContext, rectangles: List[tuple]) -> List[dict]:
44
+ """Extract Rectangle patches (bar/hist)."""
45
+ from .._csv import _get_csv_column_names
46
+
47
+ artists = []
48
+ is_bar = ctx.plot_type in ("bar", "barh")
49
+ is_hist = ctx.plot_type == "hist"
50
+
51
+ # Get trace_id from history
52
+ trace_id_for_bars = _get_bar_trace_id(ctx)
53
+
54
+ bar_count = 0
55
+ for rect_idx, (i, patch) in enumerate(rectangles):
56
+ scitex_id = getattr(patch, "_scitex_id", None)
57
+ label = patch.get_label() if hasattr(patch, "get_label") else ""
58
+
59
+ # Skip internal patches for non-bar/hist types
60
+ if not (is_bar or is_hist):
61
+ if (
62
+ ctx.skip_unlabeled
63
+ and not scitex_id
64
+ and (not label or label.startswith("_"))
65
+ ):
66
+ continue
67
+
68
+ artist = {}
69
+
70
+ # Generate unique ID
71
+ base_id = scitex_id or (
72
+ label if label and not label.startswith("_") else trace_id_for_bars or "bar"
73
+ )
74
+ artist["id"] = f"{base_id}_{bar_count}"
75
+ artist["group_id"] = base_id
76
+
77
+ # Semantic layer
78
+ artist["mark"] = "bar"
79
+ if is_hist:
80
+ artist["role"] = "hist_bin"
81
+ else:
82
+ artist["role"] = "bar_body"
83
+
84
+ # Legend
85
+ if label and not label.startswith("_") and bar_count == 0:
86
+ artist["label"] = label
87
+ artist["legend_included"] = True
88
+ else:
89
+ artist["legend_included"] = False
90
+
91
+ artist["zorder"] = patch.get_zorder()
92
+
93
+ # Backend layer
94
+ backend = {
95
+ "name": "matplotlib",
96
+ "artist_class": type(patch).__name__,
97
+ "props": {},
98
+ }
99
+
100
+ try:
101
+ backend["props"]["facecolor"] = color_to_hex(patch.get_facecolor())
102
+ except (ValueError, TypeError):
103
+ pass
104
+
105
+ try:
106
+ backend["props"]["edgecolor"] = color_to_hex(patch.get_edgecolor())
107
+ except (ValueError, TypeError):
108
+ pass
109
+
110
+ try:
111
+ backend["props"]["linewidth_pt"] = patch.get_linewidth()
112
+ except (ValueError, TypeError):
113
+ pass
114
+
115
+ artist["backend"] = backend
116
+
117
+ # Geometry
118
+ try:
119
+ artist["geometry"] = {
120
+ "x": patch.get_x(),
121
+ "y": patch.get_y(),
122
+ "width": patch.get_width(),
123
+ "height": patch.get_height(),
124
+ }
125
+ except (ValueError, TypeError, AttributeError):
126
+ pass
127
+
128
+ # Data reference
129
+ if trace_id_for_bars:
130
+ artist["data_ref"] = _get_csv_column_names(
131
+ trace_id_for_bars, ctx.ax_row, ctx.ax_col
132
+ )
133
+ artist["data_ref"]["row_index"] = bar_count
134
+
135
+ bar_count += 1
136
+ artists.append(artist)
137
+
138
+ return artists
139
+
140
+
141
+ def _get_bar_trace_id(ctx: ExtractionContext) -> Optional[str]:
142
+ """Get trace ID for bar/hist from history."""
143
+ if hasattr(ctx.ax_for_detection, "history"):
144
+ for record in ctx.ax_for_detection.history.values():
145
+ if isinstance(record, tuple) and len(record) >= 2:
146
+ method_name = record[1]
147
+ if method_name in ("bar", "barh", "hist"):
148
+ return record[0]
149
+ return None
150
+
151
+
152
+ def _extract_wedge(ctx: ExtractionContext, index: int, patch) -> dict:
153
+ """Extract Wedge (pie) patch."""
154
+ artist = {}
155
+ scitex_id = getattr(patch, "_scitex_id", None)
156
+ label = patch.get_label() if hasattr(patch, "get_label") else ""
157
+
158
+ if scitex_id:
159
+ artist["id"] = scitex_id
160
+ elif label and not label.startswith("_"):
161
+ artist["id"] = label
162
+ else:
163
+ artist["id"] = f"wedge_{index}"
164
+
165
+ artist["mark"] = "wedge"
166
+ artist["role"] = "pie_slice"
167
+
168
+ if label and not label.startswith("_"):
169
+ artist["label"] = label
170
+ artist["legend_included"] = True
171
+ else:
172
+ artist["legend_included"] = False
173
+
174
+ artist["zorder"] = patch.get_zorder()
175
+
176
+ # Backend layer
177
+ backend = {
178
+ "name": "matplotlib",
179
+ "artist_class": type(patch).__name__,
180
+ "props": {},
181
+ }
182
+
183
+ try:
184
+ backend["props"]["facecolor"] = color_to_hex(patch.get_facecolor())
185
+ except (ValueError, TypeError):
186
+ pass
187
+
188
+ try:
189
+ backend["props"]["edgecolor"] = color_to_hex(patch.get_edgecolor())
190
+ except (ValueError, TypeError):
191
+ pass
192
+
193
+ artist["backend"] = backend
194
+
195
+ # Geometry
196
+ try:
197
+ artist["geometry"] = {
198
+ "theta1": patch.theta1,
199
+ "theta2": patch.theta2,
200
+ "r": patch.r,
201
+ }
202
+ except (ValueError, TypeError, AttributeError):
203
+ pass
204
+
205
+ return artist
206
+
207
+
208
+ def _extract_polygon(ctx: ExtractionContext, index: int, patch) -> dict:
209
+ """Extract Polygon patch."""
210
+ artist = {}
211
+ scitex_id = getattr(patch, "_scitex_id", None)
212
+ label = patch.get_label() if hasattr(patch, "get_label") else ""
213
+
214
+ if scitex_id:
215
+ artist["id"] = scitex_id
216
+ elif label and not label.startswith("_"):
217
+ artist["id"] = label
218
+ else:
219
+ artist["id"] = f"polygon_{index}"
220
+
221
+ artist["mark"] = "polygon"
222
+ artist["legend_included"] = False
223
+ artist["zorder"] = patch.get_zorder()
224
+
225
+ # Backend layer
226
+ backend = {
227
+ "name": "matplotlib",
228
+ "artist_class": type(patch).__name__,
229
+ "props": {},
230
+ }
231
+
232
+ try:
233
+ backend["props"]["facecolor"] = color_to_hex(patch.get_facecolor())
234
+ except (ValueError, TypeError):
235
+ pass
236
+
237
+ try:
238
+ backend["props"]["edgecolor"] = color_to_hex(patch.get_edgecolor())
239
+ except (ValueError, TypeError):
240
+ pass
241
+
242
+ artist["backend"] = backend
243
+
244
+ return artist
245
+
246
+
247
+ # EOF