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,80 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: "2026-01-24 (ywatanabe)"
3
+ # File: /home/ywatanabe/proj/scitex-python/src/scitex/plt/utils/_metadata/__init__.py
4
+
5
+ """
6
+ Figure metadata extraction package.
7
+
8
+ This package provides modular utilities for extracting metadata from
9
+ matplotlib figures, split from the original _collect_figure_metadata.py.
10
+
11
+ Modules:
12
+ - _rounding: Precision-controlled rounding utilities
13
+ - _detect: Plot type detection
14
+ - _csv: CSV column naming and hash computation
15
+ - _verification: CSV/JSON consistency verification
16
+ - _legend: Legend extraction
17
+ - _artists: Artist extraction (lines, collections, patches, images, text)
18
+ """
19
+
20
+ # Rounding utilities
21
+ # Artist extraction
22
+ from ._artists import _extract_artists, _extract_traces
23
+
24
+ # CSV utilities
25
+ from ._csv import (
26
+ _compute_csv_hash,
27
+ _compute_csv_hash_from_df,
28
+ _extract_csv_columns_from_history,
29
+ _get_csv_column_names,
30
+ _get_csv_columns_for_method,
31
+ _get_csv_columns_for_method_with_index,
32
+ )
33
+
34
+ # Plot type detection
35
+ from ._detect import _detect_plot_type
36
+
37
+ # Legend extraction
38
+ from ._legend import _extract_legend_info
39
+ from ._rounding import (
40
+ PRECISION,
41
+ FixedFloat,
42
+ _round_dict,
43
+ _round_list,
44
+ _round_value,
45
+ )
46
+
47
+ # Verification
48
+ from ._verification import (
49
+ assert_csv_json_consistency,
50
+ verify_csv_json_consistency,
51
+ )
52
+
53
+ __all__ = [
54
+ # Rounding
55
+ "PRECISION",
56
+ "FixedFloat",
57
+ "_round_value",
58
+ "_round_list",
59
+ "_round_dict",
60
+ # Detection
61
+ "_detect_plot_type",
62
+ # CSV
63
+ "_get_csv_column_names",
64
+ "_extract_csv_columns_from_history",
65
+ "_get_csv_columns_for_method_with_index",
66
+ "_get_csv_columns_for_method",
67
+ "_compute_csv_hash_from_df",
68
+ "_compute_csv_hash",
69
+ # Verification
70
+ "assert_csv_json_consistency",
71
+ "verify_csv_json_consistency",
72
+ # Legend
73
+ "_extract_legend_info",
74
+ # Artists
75
+ "_extract_artists",
76
+ "_extract_traces",
77
+ ]
78
+
79
+
80
+ # EOF
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: "2026-01-24 (ywatanabe)"
3
+ # File: /home/ywatanabe/proj/scitex-python/src/scitex/plt/utils/_metadata/_artists/__init__.py
4
+
5
+ """
6
+ Artist extraction for figure metadata.
7
+
8
+ This package splits the large _extract_artists function into logical modules:
9
+ - _base: Common utilities and context setup
10
+ - _lines: Line2D artist extraction (including boxplot/violin/stem semantics)
11
+ - _collections: Collection artist extraction (scatter, hexbin, violin bodies)
12
+ - _patches: Patch artist extraction (bar, pie, histogram)
13
+ - _images: Image artist extraction
14
+ - _text: Text artist extraction
15
+ """
16
+
17
+ from ._extract import _extract_artists
18
+
19
+ # Backward compatibility alias
20
+ _extract_traces = _extract_artists
21
+
22
+ __all__ = ["_extract_artists", "_extract_traces"]
23
+
24
+
25
+ # EOF
@@ -0,0 +1,195 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: "2026-01-24 (ywatanabe)"
3
+ # File: /home/ywatanabe/proj/scitex-python/src/scitex/plt/utils/_metadata/_artists/_base.py
4
+
5
+ """
6
+ Base utilities for artist extraction.
7
+
8
+ Provides common context and helper functions used across artist extractors.
9
+ """
10
+
11
+ from dataclasses import dataclass, field
12
+ from typing import Any, Dict, List, Optional
13
+
14
+ import matplotlib.colors as mcolors
15
+
16
+
17
+ @dataclass
18
+ class ExtractionContext:
19
+ """Context for artist extraction operations."""
20
+
21
+ ax: Any # matplotlib axes
22
+ mpl_ax: Any # raw matplotlib axes
23
+ ax_for_detection: Any # axes for plot type detection
24
+ ax_row: int = 0
25
+ ax_col: int = 0
26
+ plot_type: Optional[str] = None
27
+ method: Optional[str] = None
28
+ skip_unlabeled: bool = False
29
+ id_to_history: Dict[str, tuple] = field(default_factory=dict)
30
+
31
+ # Boxplot specific
32
+ is_boxplot: bool = False
33
+ num_boxes: int = 0
34
+ boxplot_data: Optional[List] = None
35
+ boxplot_stats: List[dict] = field(default_factory=list)
36
+
37
+ # Violin specific
38
+ is_violin: bool = False
39
+
40
+ # Stem specific
41
+ is_stem: bool = False
42
+
43
+
44
+ def create_extraction_context(ax) -> ExtractionContext:
45
+ """Create an extraction context from axes."""
46
+ from .._detect import _detect_plot_type
47
+
48
+ # Get axes position for CSV column naming
49
+ ax_row, ax_col = 0, 0
50
+ if hasattr(ax, "_scitex_metadata") and "position_in_grid" in ax._scitex_metadata:
51
+ pos = ax._scitex_metadata["position_in_grid"]
52
+ ax_row, ax_col = pos[0], pos[1]
53
+
54
+ # Get the raw matplotlib axes
55
+ mpl_ax = ax._axis_mpl if hasattr(ax, "_axis_mpl") else ax
56
+
57
+ # Try to find scitex wrapper for plot type detection
58
+ ax_for_detection = ax
59
+ if not hasattr(ax, "history") and hasattr(mpl_ax, "_scitex_wrapper"):
60
+ ax_for_detection = mpl_ax._scitex_wrapper
61
+
62
+ # Detect plot type
63
+ plot_type, method = _detect_plot_type(ax_for_detection)
64
+
65
+ # Plot types where internal artists should be hidden
66
+ internal_plot_types = {
67
+ "boxplot",
68
+ "violin",
69
+ "hist",
70
+ "bar",
71
+ "image",
72
+ "heatmap",
73
+ "kde",
74
+ "ecdf",
75
+ "errorbar",
76
+ "fill",
77
+ "stem",
78
+ "contour",
79
+ "pie",
80
+ "quiver",
81
+ "stream",
82
+ }
83
+ skip_unlabeled = plot_type in internal_plot_types
84
+
85
+ # Build history map
86
+ id_to_history = {}
87
+ if hasattr(ax_for_detection, "history"):
88
+ for record_id, record in ax_for_detection.history.items():
89
+ if isinstance(record, tuple) and len(record) >= 2:
90
+ tracking_id = record[0]
91
+ id_to_history[tracking_id] = record
92
+
93
+ ctx = ExtractionContext(
94
+ ax=ax,
95
+ mpl_ax=mpl_ax,
96
+ ax_for_detection=ax_for_detection,
97
+ ax_row=ax_row,
98
+ ax_col=ax_col,
99
+ plot_type=plot_type,
100
+ method=method,
101
+ skip_unlabeled=skip_unlabeled,
102
+ id_to_history=id_to_history,
103
+ is_boxplot=plot_type == "boxplot",
104
+ is_violin=plot_type == "violin",
105
+ is_stem=plot_type == "stem",
106
+ )
107
+
108
+ # Extract boxplot info
109
+ if ctx.is_boxplot:
110
+ _extract_boxplot_info(ctx)
111
+
112
+ return ctx
113
+
114
+
115
+ def _extract_boxplot_info(ctx: ExtractionContext) -> None:
116
+ """Extract boxplot specific information."""
117
+ import numpy as np
118
+
119
+ if not hasattr(ctx.ax_for_detection, "history"):
120
+ return
121
+
122
+ for record in ctx.ax_for_detection.history.values():
123
+ if isinstance(record, tuple) and len(record) >= 3:
124
+ method_name = record[1]
125
+ if method_name == "boxplot":
126
+ tracked_dict = record[2]
127
+ args = tracked_dict.get("args", [])
128
+ if args and len(args) > 0:
129
+ data = args[0]
130
+ if hasattr(data, "__len__") and not isinstance(data, str):
131
+ if hasattr(data[0], "__len__") and not isinstance(data[0], str):
132
+ ctx.num_boxes = len(data)
133
+ ctx.boxplot_data = data
134
+ else:
135
+ ctx.num_boxes = 1
136
+ ctx.boxplot_data = [data]
137
+ break
138
+
139
+ # Compute boxplot statistics
140
+ if ctx.boxplot_data is not None:
141
+ for box_idx, box_data in enumerate(ctx.boxplot_data):
142
+ try:
143
+ arr = np.asarray(box_data)
144
+ arr = arr[~np.isnan(arr)]
145
+ if len(arr) > 0:
146
+ q1 = float(np.percentile(arr, 25))
147
+ median = float(np.median(arr))
148
+ q3 = float(np.percentile(arr, 75))
149
+ iqr = q3 - q1
150
+ whisker_low = float(max(arr.min(), q1 - 1.5 * iqr))
151
+ whisker_high = float(min(arr.max(), q3 + 1.5 * iqr))
152
+ fliers = arr[(arr < whisker_low) | (arr > whisker_high)]
153
+ ctx.boxplot_stats.append(
154
+ {
155
+ "box_index": box_idx,
156
+ "median": median,
157
+ "q1": q1,
158
+ "q3": q3,
159
+ "whisker_low": whisker_low,
160
+ "whisker_high": whisker_high,
161
+ "n_fliers": int(len(fliers)),
162
+ "n_samples": int(len(arr)),
163
+ }
164
+ )
165
+ except (ValueError, TypeError):
166
+ pass
167
+
168
+
169
+ def color_to_hex(color) -> Optional[str]:
170
+ """Convert color to hex string."""
171
+ try:
172
+ return mcolors.to_hex(color, keep_alpha=False)
173
+ except (ValueError, TypeError):
174
+ return None
175
+
176
+
177
+ def get_artist_id(
178
+ obj,
179
+ index: int,
180
+ prefix: str,
181
+ scitex_id: Optional[str] = None,
182
+ label: Optional[str] = None,
183
+ semantic_id: Optional[str] = None,
184
+ ) -> str:
185
+ """Get a unique ID for an artist."""
186
+ if scitex_id:
187
+ return scitex_id
188
+ if semantic_id:
189
+ return semantic_id
190
+ if label and not label.startswith("_"):
191
+ return label
192
+ return f"{prefix}_{index}"
193
+
194
+
195
+ # EOF
@@ -0,0 +1,356 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: "2026-01-24 (ywatanabe)"
3
+ # File: /home/ywatanabe/proj/scitex-python/src/scitex/plt/utils/_metadata/_artists/_collections.py
4
+
5
+ """
6
+ Collection artist extraction.
7
+
8
+ Handles PathCollection (scatter), PolyCollection (hexbin, violin),
9
+ QuadMesh (hist2d), and LineCollection (errorbar) extraction.
10
+ """
11
+
12
+ from typing import List
13
+
14
+ from ._base import ExtractionContext, color_to_hex
15
+
16
+
17
+ def extract_collections(ctx: ExtractionContext) -> List[dict]:
18
+ """Extract collection artists from axes."""
19
+ artists = []
20
+
21
+ for i, coll in enumerate(ctx.mpl_ax.collections):
22
+ coll_type = type(coll).__name__
23
+
24
+ if "PathCollection" in coll_type:
25
+ artist = _extract_scatter(ctx, i, coll)
26
+ if artist:
27
+ artists.append(artist)
28
+ elif "PolyCollection" in coll_type:
29
+ artist = _extract_poly_collection(ctx, i, coll)
30
+ if artist:
31
+ artists.append(artist)
32
+ elif "QuadMesh" in coll_type:
33
+ artist = _extract_quadmesh(ctx, i, coll)
34
+ if artist:
35
+ artists.append(artist)
36
+ elif coll_type == "LineCollection":
37
+ artist = _extract_line_collection(ctx, i, coll)
38
+ if artist:
39
+ artists.append(artist)
40
+
41
+ return artists
42
+
43
+
44
+ def _extract_scatter(ctx: ExtractionContext, index: int, coll) -> dict:
45
+ """Extract PathCollection (scatter) artist."""
46
+ from .._csv import _get_csv_column_names
47
+
48
+ artist = {}
49
+ scitex_id = getattr(coll, "_scitex_id", None)
50
+ label = coll.get_label()
51
+
52
+ if scitex_id:
53
+ artist["id"] = scitex_id
54
+ elif label and not label.startswith("_"):
55
+ artist["id"] = label
56
+ else:
57
+ artist["id"] = f"scatter_{index}"
58
+
59
+ artist["mark"] = "scatter"
60
+
61
+ if label and not label.startswith("_"):
62
+ artist["label"] = label
63
+ artist["legend_included"] = True
64
+ else:
65
+ artist["legend_included"] = False
66
+
67
+ artist["zorder"] = coll.get_zorder()
68
+
69
+ # Backend layer
70
+ backend = {
71
+ "name": "matplotlib",
72
+ "artist_class": type(coll).__name__,
73
+ "props": {},
74
+ }
75
+
76
+ try:
77
+ facecolors = coll.get_facecolor()
78
+ if len(facecolors) > 0:
79
+ backend["props"]["facecolor"] = color_to_hex(facecolors[0])
80
+ except (ValueError, TypeError, IndexError):
81
+ pass
82
+
83
+ try:
84
+ edgecolors = coll.get_edgecolor()
85
+ if len(edgecolors) > 0:
86
+ backend["props"]["edgecolor"] = color_to_hex(edgecolors[0])
87
+ except (ValueError, TypeError, IndexError):
88
+ pass
89
+
90
+ try:
91
+ sizes = coll.get_sizes()
92
+ if len(sizes) > 0:
93
+ backend["props"]["size"] = float(sizes[0])
94
+ except (ValueError, TypeError, IndexError):
95
+ pass
96
+
97
+ artist["backend"] = backend
98
+
99
+ # Data reference
100
+ artist_id = artist.get("id", str(index))
101
+ artist["data_ref"] = _get_csv_column_names(artist_id, ctx.ax_row, ctx.ax_col)
102
+
103
+ return artist
104
+
105
+
106
+ def _extract_poly_collection(ctx: ExtractionContext, index: int, coll) -> dict:
107
+ """Extract PolyCollection (hexbin, violin body) artist."""
108
+ coll_type = type(coll).__name__
109
+
110
+ # Check if hexbin
111
+ if hasattr(coll, "get_array"):
112
+ arr = coll.get_array()
113
+ if arr is not None and len(arr) > 0 and ctx.plot_type != "violin":
114
+ return _extract_hexbin(ctx, index, coll, arr)
115
+
116
+ # Violin body
117
+ if ctx.plot_type == "violin":
118
+ return _extract_violin_body(ctx, index, coll)
119
+
120
+ return None
121
+
122
+
123
+ def _extract_hexbin(ctx: ExtractionContext, index: int, coll, arr) -> dict:
124
+ """Extract hexbin PolyCollection."""
125
+ artist = {}
126
+ scitex_id = getattr(coll, "_scitex_id", None)
127
+ label = coll.get_label() if hasattr(coll, "get_label") else ""
128
+
129
+ if scitex_id:
130
+ artist["id"] = scitex_id
131
+ elif label and not label.startswith("_"):
132
+ artist["id"] = label
133
+ else:
134
+ artist["id"] = f"hexbin_{index}"
135
+
136
+ artist["mark"] = "hexbin"
137
+ artist["role"] = "hexbin"
138
+ artist["legend_included"] = False
139
+ artist["zorder"] = coll.get_zorder()
140
+
141
+ # Backend layer
142
+ backend = {
143
+ "name": "matplotlib",
144
+ "artist_class": type(coll).__name__,
145
+ "props": {},
146
+ }
147
+
148
+ try:
149
+ cmap = coll.get_cmap()
150
+ if cmap:
151
+ backend["props"]["cmap"] = cmap.name
152
+ except (ValueError, TypeError, AttributeError):
153
+ pass
154
+
155
+ try:
156
+ backend["props"]["vmin"] = float(coll.norm.vmin) if coll.norm else None
157
+ backend["props"]["vmax"] = float(coll.norm.vmax) if coll.norm else None
158
+ except (ValueError, TypeError, AttributeError):
159
+ pass
160
+
161
+ artist["backend"] = backend
162
+
163
+ # Result info
164
+ try:
165
+ artist["result"] = {
166
+ "n_hexagons": int(len(arr)),
167
+ "count_range": [float(arr.min()), float(arr.max())]
168
+ if len(arr) > 0
169
+ else None,
170
+ "total_count": int(arr.sum()),
171
+ }
172
+ except (TypeError, AttributeError, ValueError):
173
+ pass
174
+
175
+ return artist
176
+
177
+
178
+ def _extract_violin_body(ctx: ExtractionContext, index: int, coll) -> dict:
179
+ """Extract violin body PolyCollection."""
180
+ artist = {}
181
+ scitex_id = getattr(coll, "_scitex_id", None)
182
+
183
+ if scitex_id:
184
+ artist["id"] = f"{scitex_id}_body_{index}"
185
+ artist["group_id"] = scitex_id
186
+ else:
187
+ artist["id"] = f"violin_body_{index}"
188
+
189
+ artist["mark"] = "polygon"
190
+ artist["role"] = "violin_body"
191
+ artist["legend_included"] = False
192
+ artist["zorder"] = coll.get_zorder()
193
+
194
+ # Backend layer
195
+ backend = {
196
+ "name": "matplotlib",
197
+ "artist_class": type(coll).__name__,
198
+ "props": {},
199
+ }
200
+
201
+ try:
202
+ facecolors = coll.get_facecolor()
203
+ if len(facecolors) > 0:
204
+ backend["props"]["facecolor"] = color_to_hex(facecolors[0])
205
+ except (ValueError, TypeError, IndexError):
206
+ pass
207
+
208
+ try:
209
+ edgecolors = coll.get_edgecolor()
210
+ if len(edgecolors) > 0:
211
+ backend["props"]["edgecolor"] = color_to_hex(edgecolors[0])
212
+ except (ValueError, TypeError, IndexError):
213
+ pass
214
+
215
+ artist["backend"] = backend
216
+
217
+ return artist
218
+
219
+
220
+ def _extract_quadmesh(ctx: ExtractionContext, index: int, coll) -> dict:
221
+ """Extract QuadMesh (hist2d) artist."""
222
+ artist = {}
223
+ scitex_id = getattr(coll, "_scitex_id", None)
224
+ label = coll.get_label() if hasattr(coll, "get_label") else ""
225
+
226
+ if scitex_id:
227
+ artist["id"] = scitex_id
228
+ elif label and not label.startswith("_"):
229
+ artist["id"] = label
230
+ else:
231
+ artist["id"] = f"hist2d_{index}"
232
+
233
+ artist["mark"] = "hist2d"
234
+ artist["role"] = "hist2d"
235
+ artist["legend_included"] = False
236
+ artist["zorder"] = coll.get_zorder()
237
+
238
+ # Backend layer
239
+ backend = {
240
+ "name": "matplotlib",
241
+ "artist_class": type(coll).__name__,
242
+ "props": {},
243
+ }
244
+
245
+ try:
246
+ cmap = coll.get_cmap()
247
+ if cmap:
248
+ backend["props"]["cmap"] = cmap.name
249
+ except (ValueError, TypeError, AttributeError):
250
+ pass
251
+
252
+ artist["backend"] = backend
253
+
254
+ return artist
255
+
256
+
257
+ def _extract_line_collection(ctx: ExtractionContext, index: int, coll) -> dict:
258
+ """Extract LineCollection (errorbar, stem) artist."""
259
+ artist = {}
260
+ scitex_id = getattr(coll, "_scitex_id", None)
261
+ label = coll.get_label() if hasattr(coll, "get_label") else ""
262
+
263
+ if scitex_id:
264
+ artist["id"] = scitex_id
265
+ elif label and not label.startswith("_"):
266
+ artist["id"] = label
267
+ else:
268
+ artist["id"] = f"linecollection_{index}"
269
+
270
+ artist["mark"] = "line"
271
+
272
+ # Determine role
273
+ if ctx.plot_type == "bar" or ctx.method == "barh":
274
+ artist["role"] = "errorbar"
275
+ elif ctx.plot_type == "stem":
276
+ artist["role"] = "stem_stem"
277
+ artist["id"] = "stem_lines"
278
+ else:
279
+ artist["role"] = "line_collection"
280
+
281
+ artist["legend_included"] = False
282
+ artist["zorder"] = coll.get_zorder()
283
+
284
+ # Backend layer
285
+ backend = {
286
+ "name": "matplotlib",
287
+ "artist_class": type(coll).__name__,
288
+ "props": {},
289
+ }
290
+
291
+ try:
292
+ colors = coll.get_colors()
293
+ if len(colors) > 0:
294
+ backend["props"]["color"] = color_to_hex(colors[0])
295
+ except (ValueError, TypeError, IndexError):
296
+ pass
297
+
298
+ try:
299
+ linewidths = coll.get_linewidths()
300
+ if len(linewidths) > 0:
301
+ backend["props"]["linewidth_pt"] = float(linewidths[0])
302
+ except (ValueError, TypeError, IndexError):
303
+ pass
304
+
305
+ artist["backend"] = backend
306
+
307
+ # Data reference for errorbar/stem
308
+ if artist["role"] == "errorbar":
309
+ _add_errorbar_data_ref(ctx, artist)
310
+ elif artist["role"] == "stem_stem":
311
+ _add_stem_data_ref(ctx, artist)
312
+
313
+ return artist
314
+
315
+
316
+ def _add_errorbar_data_ref(ctx: ExtractionContext, artist: dict) -> None:
317
+ """Add data_ref for errorbar LineCollection."""
318
+ from .._csv import _get_csv_column_names
319
+
320
+ errorbar_trace_id = None
321
+ error_var = "yerr" if ctx.method == "bar" else "xerr"
322
+
323
+ if hasattr(ctx.ax_for_detection, "history"):
324
+ for record in ctx.ax_for_detection.history.values():
325
+ if isinstance(record, tuple) and len(record) >= 2:
326
+ method_name = record[1]
327
+ if method_name in ("bar", "barh"):
328
+ errorbar_trace_id = record[0]
329
+ break
330
+
331
+ if errorbar_trace_id:
332
+ base_ref = _get_csv_column_names(errorbar_trace_id, ctx.ax_row, ctx.ax_col)
333
+ artist["data_ref"] = {
334
+ "x": base_ref.get("x"),
335
+ "y": base_ref.get("y"),
336
+ error_var: f"ax-row-{ctx.ax_row}-col-{ctx.ax_col}_trace-id-{errorbar_trace_id}_variable-{error_var}",
337
+ }
338
+
339
+
340
+ def _add_stem_data_ref(ctx: ExtractionContext, artist: dict) -> None:
341
+ """Add data_ref for stem LineCollection."""
342
+ from .._csv import _get_csv_column_names
343
+
344
+ if hasattr(ctx.ax_for_detection, "history"):
345
+ for record in ctx.ax_for_detection.history.values():
346
+ if isinstance(record, tuple) and len(record) >= 2:
347
+ method_name = record[1]
348
+ if method_name == "stem":
349
+ stem_trace_id = record[0]
350
+ artist["data_ref"] = _get_csv_column_names(
351
+ stem_trace_id, ctx.ax_row, ctx.ax_col
352
+ )
353
+ break
354
+
355
+
356
+ # EOF