scitex 2.14.0__py3-none-any.whl → 2.15.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (264) hide show
  1. scitex/__init__.py +71 -17
  2. scitex/_env_loader.py +156 -0
  3. scitex/_mcp_resources/__init__.py +37 -0
  4. scitex/_mcp_resources/_cheatsheet.py +135 -0
  5. scitex/_mcp_resources/_figrecipe.py +138 -0
  6. scitex/_mcp_resources/_formats.py +102 -0
  7. scitex/_mcp_resources/_modules.py +337 -0
  8. scitex/_mcp_resources/_session.py +149 -0
  9. scitex/_mcp_tools/__init__.py +4 -0
  10. scitex/_mcp_tools/audio.py +66 -0
  11. scitex/_mcp_tools/diagram.py +11 -95
  12. scitex/_mcp_tools/introspect.py +210 -0
  13. scitex/_mcp_tools/plt.py +260 -305
  14. scitex/_mcp_tools/scholar.py +74 -0
  15. scitex/_mcp_tools/social.py +27 -0
  16. scitex/_mcp_tools/template.py +24 -0
  17. scitex/_mcp_tools/writer.py +17 -210
  18. scitex/ai/_gen_ai/_PARAMS.py +10 -7
  19. scitex/ai/classification/reporters/_SingleClassificationReporter.py +45 -1603
  20. scitex/ai/classification/reporters/_mixins/__init__.py +36 -0
  21. scitex/ai/classification/reporters/_mixins/_constants.py +67 -0
  22. scitex/ai/classification/reporters/_mixins/_cv_summary.py +387 -0
  23. scitex/ai/classification/reporters/_mixins/_feature_importance.py +119 -0
  24. scitex/ai/classification/reporters/_mixins/_metrics.py +275 -0
  25. scitex/ai/classification/reporters/_mixins/_plotting.py +179 -0
  26. scitex/ai/classification/reporters/_mixins/_reports.py +153 -0
  27. scitex/ai/classification/reporters/_mixins/_storage.py +160 -0
  28. scitex/ai/classification/timeseries/_TimeSeriesSlidingWindowSplit.py +30 -1550
  29. scitex/ai/classification/timeseries/_sliding_window_core.py +467 -0
  30. scitex/ai/classification/timeseries/_sliding_window_plotting.py +369 -0
  31. scitex/audio/README.md +40 -36
  32. scitex/audio/__init__.py +129 -61
  33. scitex/audio/_branding.py +185 -0
  34. scitex/audio/_mcp/__init__.py +32 -0
  35. scitex/audio/_mcp/handlers.py +59 -6
  36. scitex/audio/_mcp/speak_handlers.py +238 -0
  37. scitex/audio/_relay.py +225 -0
  38. scitex/audio/_tts.py +18 -10
  39. scitex/audio/engines/base.py +17 -10
  40. scitex/audio/engines/elevenlabs_engine.py +7 -2
  41. scitex/audio/mcp_server.py +228 -75
  42. scitex/canvas/README.md +1 -1
  43. scitex/canvas/editor/_dearpygui/__init__.py +25 -0
  44. scitex/canvas/editor/_dearpygui/_editor.py +147 -0
  45. scitex/canvas/editor/_dearpygui/_handlers.py +476 -0
  46. scitex/canvas/editor/_dearpygui/_panels/__init__.py +17 -0
  47. scitex/canvas/editor/_dearpygui/_panels/_control.py +119 -0
  48. scitex/canvas/editor/_dearpygui/_panels/_element_controls.py +190 -0
  49. scitex/canvas/editor/_dearpygui/_panels/_preview.py +43 -0
  50. scitex/canvas/editor/_dearpygui/_panels/_sections.py +390 -0
  51. scitex/canvas/editor/_dearpygui/_plotting.py +187 -0
  52. scitex/canvas/editor/_dearpygui/_rendering.py +504 -0
  53. scitex/canvas/editor/_dearpygui/_selection.py +295 -0
  54. scitex/canvas/editor/_dearpygui/_state.py +93 -0
  55. scitex/canvas/editor/_dearpygui/_utils.py +61 -0
  56. scitex/canvas/editor/flask_editor/_core/__init__.py +27 -0
  57. scitex/canvas/editor/flask_editor/_core/_bbox_extraction.py +200 -0
  58. scitex/canvas/editor/flask_editor/_core/_editor.py +173 -0
  59. scitex/canvas/editor/flask_editor/_core/_export_helpers.py +353 -0
  60. scitex/canvas/editor/flask_editor/_core/_routes_basic.py +190 -0
  61. scitex/canvas/editor/flask_editor/_core/_routes_export.py +332 -0
  62. scitex/canvas/editor/flask_editor/_core/_routes_panels.py +252 -0
  63. scitex/canvas/editor/flask_editor/_core/_routes_save.py +218 -0
  64. scitex/canvas/editor/flask_editor/_core.py +25 -1684
  65. scitex/canvas/editor/flask_editor/templates/__init__.py +32 -70
  66. scitex/cli/__init__.py +38 -43
  67. scitex/cli/audio.py +160 -41
  68. scitex/cli/capture.py +133 -20
  69. scitex/cli/introspect.py +488 -0
  70. scitex/cli/main.py +200 -109
  71. scitex/cli/mcp.py +60 -34
  72. scitex/cli/plt.py +414 -0
  73. scitex/cli/repro.py +15 -8
  74. scitex/cli/resource.py +15 -8
  75. scitex/cli/scholar/__init__.py +154 -8
  76. scitex/cli/scholar/_crossref_scitex.py +296 -0
  77. scitex/cli/scholar/_fetch.py +25 -3
  78. scitex/cli/social.py +355 -0
  79. scitex/cli/stats.py +136 -11
  80. scitex/cli/template.py +129 -12
  81. scitex/cli/tex.py +15 -8
  82. scitex/cli/writer.py +49 -299
  83. scitex/cloud/__init__.py +41 -2
  84. scitex/config/README.md +1 -1
  85. scitex/config/__init__.py +16 -2
  86. scitex/config/_env_registry.py +256 -0
  87. scitex/context/__init__.py +22 -0
  88. scitex/dev/__init__.py +20 -1
  89. scitex/diagram/__init__.py +42 -19
  90. scitex/diagram/mcp_server.py +13 -125
  91. scitex/gen/__init__.py +50 -14
  92. scitex/gen/_list_packages.py +4 -4
  93. scitex/introspect/__init__.py +82 -0
  94. scitex/introspect/_call_graph.py +303 -0
  95. scitex/introspect/_class_hierarchy.py +163 -0
  96. scitex/introspect/_core.py +41 -0
  97. scitex/introspect/_docstring.py +131 -0
  98. scitex/introspect/_examples.py +113 -0
  99. scitex/introspect/_imports.py +271 -0
  100. scitex/{gen/_inspect_module.py → introspect/_list_api.py} +48 -56
  101. scitex/introspect/_mcp/__init__.py +41 -0
  102. scitex/introspect/_mcp/handlers.py +233 -0
  103. scitex/introspect/_members.py +155 -0
  104. scitex/introspect/_resolve.py +89 -0
  105. scitex/introspect/_signature.py +131 -0
  106. scitex/introspect/_source.py +80 -0
  107. scitex/introspect/_type_hints.py +172 -0
  108. scitex/io/_save.py +1 -2
  109. scitex/io/bundle/README.md +1 -1
  110. scitex/logging/_formatters.py +19 -9
  111. scitex/mcp_server.py +98 -5
  112. scitex/os/__init__.py +4 -0
  113. scitex/{gen → os}/_check_host.py +4 -5
  114. scitex/plt/__init__.py +245 -550
  115. scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin/_wrappers.py +5 -10
  116. scitex/plt/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
  117. scitex/plt/gallery/README.md +1 -1
  118. scitex/plt/utils/_hitmap/__init__.py +82 -0
  119. scitex/plt/utils/_hitmap/_artist_extraction.py +343 -0
  120. scitex/plt/utils/_hitmap/_color_application.py +346 -0
  121. scitex/plt/utils/_hitmap/_color_conversion.py +121 -0
  122. scitex/plt/utils/_hitmap/_constants.py +40 -0
  123. scitex/plt/utils/_hitmap/_hitmap_core.py +334 -0
  124. scitex/plt/utils/_hitmap/_path_extraction.py +357 -0
  125. scitex/plt/utils/_hitmap/_query.py +113 -0
  126. scitex/plt/utils/_hitmap.py +46 -1616
  127. scitex/plt/utils/_metadata/__init__.py +80 -0
  128. scitex/plt/utils/_metadata/_artists/__init__.py +25 -0
  129. scitex/plt/utils/_metadata/_artists/_base.py +195 -0
  130. scitex/plt/utils/_metadata/_artists/_collections.py +356 -0
  131. scitex/plt/utils/_metadata/_artists/_extract.py +57 -0
  132. scitex/plt/utils/_metadata/_artists/_images.py +80 -0
  133. scitex/plt/utils/_metadata/_artists/_lines.py +261 -0
  134. scitex/plt/utils/_metadata/_artists/_patches.py +247 -0
  135. scitex/plt/utils/_metadata/_artists/_text.py +106 -0
  136. scitex/plt/utils/_metadata/_csv.py +416 -0
  137. scitex/plt/utils/_metadata/_detect.py +225 -0
  138. scitex/plt/utils/_metadata/_legend.py +127 -0
  139. scitex/plt/utils/_metadata/_rounding.py +117 -0
  140. scitex/plt/utils/_metadata/_verification.py +202 -0
  141. scitex/schema/README.md +1 -1
  142. scitex/scholar/__init__.py +8 -0
  143. scitex/scholar/_mcp/crossref_handlers.py +265 -0
  144. scitex/scholar/core/Scholar.py +63 -1700
  145. scitex/scholar/core/_mixins/__init__.py +36 -0
  146. scitex/scholar/core/_mixins/_enrichers.py +270 -0
  147. scitex/scholar/core/_mixins/_library_handlers.py +100 -0
  148. scitex/scholar/core/_mixins/_loaders.py +103 -0
  149. scitex/scholar/core/_mixins/_pdf_download.py +375 -0
  150. scitex/scholar/core/_mixins/_pipeline.py +312 -0
  151. scitex/scholar/core/_mixins/_project_handlers.py +125 -0
  152. scitex/scholar/core/_mixins/_savers.py +69 -0
  153. scitex/scholar/core/_mixins/_search.py +103 -0
  154. scitex/scholar/core/_mixins/_services.py +88 -0
  155. scitex/scholar/core/_mixins/_url_finding.py +105 -0
  156. scitex/scholar/crossref_scitex.py +367 -0
  157. scitex/scholar/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
  158. scitex/scholar/examples/00_run_all.sh +120 -0
  159. scitex/scholar/jobs/_executors.py +27 -3
  160. scitex/scholar/pdf_download/ScholarPDFDownloader.py +38 -416
  161. scitex/scholar/pdf_download/_cli.py +154 -0
  162. scitex/scholar/pdf_download/strategies/__init__.py +11 -8
  163. scitex/scholar/pdf_download/strategies/manual_download_fallback.py +80 -3
  164. scitex/scholar/pipelines/ScholarPipelineBibTeX.py +73 -121
  165. scitex/scholar/pipelines/ScholarPipelineParallel.py +80 -138
  166. scitex/scholar/pipelines/ScholarPipelineSingle.py +43 -63
  167. scitex/scholar/pipelines/_single_steps.py +71 -36
  168. scitex/scholar/storage/_LibraryManager.py +97 -1695
  169. scitex/scholar/storage/_mixins/__init__.py +30 -0
  170. scitex/scholar/storage/_mixins/_bibtex_handlers.py +128 -0
  171. scitex/scholar/storage/_mixins/_library_operations.py +218 -0
  172. scitex/scholar/storage/_mixins/_metadata_conversion.py +226 -0
  173. scitex/scholar/storage/_mixins/_paper_saving.py +456 -0
  174. scitex/scholar/storage/_mixins/_resolution.py +376 -0
  175. scitex/scholar/storage/_mixins/_storage_helpers.py +121 -0
  176. scitex/scholar/storage/_mixins/_symlink_handlers.py +226 -0
  177. scitex/security/README.md +3 -3
  178. scitex/session/README.md +1 -1
  179. scitex/session/__init__.py +26 -7
  180. scitex/session/_decorator.py +1 -1
  181. scitex/sh/README.md +1 -1
  182. scitex/sh/__init__.py +7 -4
  183. scitex/social/__init__.py +155 -0
  184. scitex/social/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
  185. scitex/stats/_mcp/_handlers/__init__.py +31 -0
  186. scitex/stats/_mcp/_handlers/_corrections.py +113 -0
  187. scitex/stats/_mcp/_handlers/_descriptive.py +78 -0
  188. scitex/stats/_mcp/_handlers/_effect_size.py +106 -0
  189. scitex/stats/_mcp/_handlers/_format.py +94 -0
  190. scitex/stats/_mcp/_handlers/_normality.py +110 -0
  191. scitex/stats/_mcp/_handlers/_posthoc.py +224 -0
  192. scitex/stats/_mcp/_handlers/_power.py +247 -0
  193. scitex/stats/_mcp/_handlers/_recommend.py +102 -0
  194. scitex/stats/_mcp/_handlers/_run_test.py +279 -0
  195. scitex/stats/_mcp/_handlers/_stars.py +48 -0
  196. scitex/stats/_mcp/handlers.py +19 -1171
  197. scitex/stats/auto/_stat_style.py +175 -0
  198. scitex/stats/auto/_style_definitions.py +411 -0
  199. scitex/stats/auto/_styles.py +22 -620
  200. scitex/stats/descriptive/__init__.py +11 -8
  201. scitex/stats/descriptive/_ci.py +39 -0
  202. scitex/stats/power/_power.py +15 -4
  203. scitex/str/__init__.py +2 -1
  204. scitex/str/_title_case.py +63 -0
  205. scitex/template/README.md +1 -1
  206. scitex/template/__init__.py +25 -10
  207. scitex/template/_code_templates.py +147 -0
  208. scitex/template/_mcp/handlers.py +81 -0
  209. scitex/template/_mcp/tool_schemas.py +55 -0
  210. scitex/template/_templates/__init__.py +51 -0
  211. scitex/template/_templates/audio.py +233 -0
  212. scitex/template/_templates/canvas.py +312 -0
  213. scitex/template/_templates/capture.py +268 -0
  214. scitex/template/_templates/config.py +43 -0
  215. scitex/template/_templates/diagram.py +294 -0
  216. scitex/template/_templates/io.py +107 -0
  217. scitex/template/_templates/module.py +53 -0
  218. scitex/template/_templates/plt.py +202 -0
  219. scitex/template/_templates/scholar.py +267 -0
  220. scitex/template/_templates/session.py +130 -0
  221. scitex/template/_templates/session_minimal.py +43 -0
  222. scitex/template/_templates/session_plot.py +67 -0
  223. scitex/template/_templates/session_stats.py +77 -0
  224. scitex/template/_templates/stats.py +323 -0
  225. scitex/template/_templates/writer.py +296 -0
  226. scitex/template/clone_writer_directory.py +5 -5
  227. scitex/ui/_backends/_email.py +10 -2
  228. scitex/ui/_backends/_webhook.py +5 -1
  229. scitex/web/_search_pubmed.py +10 -6
  230. scitex/writer/README.md +1 -1
  231. scitex/writer/__init__.py +43 -34
  232. scitex/writer/_mcp/handlers.py +11 -744
  233. scitex/writer/_mcp/tool_schemas.py +5 -335
  234. scitex-2.15.3.dist-info/METADATA +667 -0
  235. {scitex-2.14.0.dist-info → scitex-2.15.3.dist-info}/RECORD +241 -120
  236. scitex/canvas/editor/flask_editor/templates/_scripts.py +0 -4933
  237. scitex/canvas/editor/flask_editor/templates/_styles.py +0 -1658
  238. scitex/diagram/_compile.py +0 -312
  239. scitex/diagram/_diagram.py +0 -355
  240. scitex/diagram/_mcp/__init__.py +0 -4
  241. scitex/diagram/_mcp/handlers.py +0 -400
  242. scitex/diagram/_mcp/tool_schemas.py +0 -157
  243. scitex/diagram/_presets.py +0 -173
  244. scitex/diagram/_schema.py +0 -182
  245. scitex/diagram/_split.py +0 -278
  246. scitex/gen/_ci.py +0 -12
  247. scitex/gen/_title_case.py +0 -89
  248. scitex/plt/_mcp/__init__.py +0 -4
  249. scitex/plt/_mcp/_handlers_annotation.py +0 -102
  250. scitex/plt/_mcp/_handlers_figure.py +0 -195
  251. scitex/plt/_mcp/_handlers_plot.py +0 -252
  252. scitex/plt/_mcp/_handlers_style.py +0 -219
  253. scitex/plt/_mcp/handlers.py +0 -74
  254. scitex/plt/_mcp/tool_schemas.py +0 -497
  255. scitex/plt/mcp_server.py +0 -231
  256. scitex/scholar/examples/SUGGESTIONS.md +0 -865
  257. scitex/scholar/examples/dev.py +0 -38
  258. scitex-2.14.0.dist-info/METADATA +0 -1238
  259. /scitex/{gen → context}/_detect_environment.py +0 -0
  260. /scitex/{gen → context}/_get_notebook_path.py +0 -0
  261. /scitex/{gen/_shell.py → sh/_shell_legacy.py} +0 -0
  262. {scitex-2.14.0.dist-info → scitex-2.15.3.dist-info}/WHEEL +0 -0
  263. {scitex-2.14.0.dist-info → scitex-2.15.3.dist-info}/entry_points.txt +0 -0
  264. {scitex-2.14.0.dist-info → scitex-2.15.3.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,225 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: "2026-01-24 (ywatanabe)"
3
+ # File: /home/ywatanabe/proj/scitex-python/src/scitex/plt/utils/_metadata/_detect.py
4
+
5
+ """
6
+ Plot type detection from axes content.
7
+
8
+ Detects the primary plot type by analyzing scitex history and matplotlib
9
+ artist content.
10
+ """
11
+
12
+ from typing import Optional, Tuple
13
+
14
+
15
+ def _detect_plot_type(ax) -> Tuple[Optional[str], Optional[str]]:
16
+ """
17
+ Detect the primary plot type and method from axes content.
18
+
19
+ Checks for:
20
+ - Lines -> "line"
21
+ - Scatter collections -> "scatter"
22
+ - Bar containers -> "bar"
23
+ - Patches (histogram) -> "hist"
24
+ - Box plot -> "boxplot"
25
+ - Violin plot -> "violin"
26
+ - Image -> "image"
27
+ - Contour -> "contour"
28
+ - KDE -> "kde"
29
+
30
+ Parameters
31
+ ----------
32
+ ax : matplotlib.axes.Axes
33
+ The axes to analyze
34
+
35
+ Returns
36
+ -------
37
+ tuple
38
+ (plot_type, method) where method is the actual plotting function used,
39
+ or (None, None) if unclear
40
+ """
41
+ # Check scitex history FIRST (most reliable for scitex plots)
42
+ result = _detect_from_history(ax)
43
+ if result[0] is not None:
44
+ return result
45
+
46
+ # Check for images (takes priority)
47
+ if len(ax.images) > 0:
48
+ return "image", "imshow"
49
+
50
+ # Check for 2D density plots (hist2d, hexbin)
51
+ result = _detect_2d_density(ax)
52
+ if result[0] is not None:
53
+ return result
54
+
55
+ # Check for contours
56
+ result = _detect_contours(ax)
57
+ if result[0] is not None:
58
+ return result
59
+
60
+ # Check for bar plots and containers
61
+ result = _detect_bars(ax)
62
+ if result[0] is not None:
63
+ return result
64
+
65
+ # Check for patches (histogram, violin, pie)
66
+ result = _detect_patches(ax)
67
+ if result[0] is not None:
68
+ return result
69
+
70
+ # Check for scatter plots (PathCollection)
71
+ result = _detect_scatter(ax)
72
+ if result[0] is not None:
73
+ return result
74
+
75
+ # Check for line plots
76
+ result = _detect_lines(ax)
77
+ if result[0] is not None:
78
+ return result
79
+
80
+ return None, None
81
+
82
+
83
+ def _detect_from_history(ax) -> Tuple[Optional[str], Optional[str]]:
84
+ """Detect plot type from scitex history."""
85
+ if not hasattr(ax, "history") or len(ax.history) == 0:
86
+ return None, None
87
+
88
+ # Method to (plot_type, method) mapping
89
+ method_map = {
90
+ "stx_heatmap": ("heatmap", "stx_heatmap"),
91
+ "stx_kde": ("kde", "stx_kde"),
92
+ "stx_ecdf": ("ecdf", "stx_ecdf"),
93
+ "stx_violin": ("violin", "stx_violin"),
94
+ "stx_box": ("boxplot", "stx_box"),
95
+ "boxplot": ("boxplot", "boxplot"),
96
+ "stx_line": ("line", "stx_line"),
97
+ "plot_scatter": ("scatter", "plot_scatter"),
98
+ "stx_mean_std": ("line", "stx_mean_std"),
99
+ "stx_mean_ci": ("line", "stx_mean_ci"),
100
+ "stx_median_iqr": ("line", "stx_median_iqr"),
101
+ "stx_shaded_line": ("line", "stx_shaded_line"),
102
+ "sns_boxplot": ("boxplot", "sns_boxplot"),
103
+ "sns_violinplot": ("violin", "sns_violinplot"),
104
+ "sns_scatterplot": ("scatter", "sns_scatterplot"),
105
+ "sns_lineplot": ("line", "sns_lineplot"),
106
+ "sns_histplot": ("hist", "sns_histplot"),
107
+ "sns_barplot": ("bar", "sns_barplot"),
108
+ "sns_stripplot": ("scatter", "sns_stripplot"),
109
+ "sns_kdeplot": ("kde", "sns_kdeplot"),
110
+ "scatter": ("scatter", "scatter"),
111
+ "bar": ("bar", "bar"),
112
+ "barh": ("bar", "barh"),
113
+ "hist": ("hist", "hist"),
114
+ "hist2d": ("hist2d", "hist2d"),
115
+ "hexbin": ("hexbin", "hexbin"),
116
+ "violinplot": ("violin", "violinplot"),
117
+ "errorbar": ("errorbar", "errorbar"),
118
+ "fill_between": ("fill", "fill_between"),
119
+ "fill_betweenx": ("fill", "fill_betweenx"),
120
+ "imshow": ("image", "imshow"),
121
+ "matshow": ("image", "matshow"),
122
+ "contour": ("contour", "contour"),
123
+ "contourf": ("contour", "contourf"),
124
+ "stem": ("stem", "stem"),
125
+ "step": ("step", "step"),
126
+ "pie": ("pie", "pie"),
127
+ "quiver": ("quiver", "quiver"),
128
+ "streamplot": ("stream", "streamplot"),
129
+ "plot": ("line", "plot"),
130
+ }
131
+
132
+ # Get all methods from history
133
+ for record in ax.history.values():
134
+ if isinstance(record, tuple) and len(record) >= 2:
135
+ method = record[1]
136
+ if method in method_map:
137
+ return method_map[method]
138
+
139
+ return None, None
140
+
141
+
142
+ def _detect_2d_density(ax) -> Tuple[Optional[str], Optional[str]]:
143
+ """Detect 2D density plots (hist2d, hexbin)."""
144
+ if not hasattr(ax, "collections"):
145
+ return None, None
146
+
147
+ for coll in ax.collections:
148
+ coll_type = type(coll).__name__
149
+ if "QuadMesh" in coll_type:
150
+ return "hist2d", "hist2d"
151
+ if "PolyCollection" in coll_type and hasattr(coll, "get_array"):
152
+ arr = coll.get_array()
153
+ if arr is not None and len(arr) > 0:
154
+ return "hexbin", "hexbin"
155
+
156
+ return None, None
157
+
158
+
159
+ def _detect_contours(ax) -> Tuple[Optional[str], Optional[str]]:
160
+ """Detect contour plots."""
161
+ if not hasattr(ax, "collections"):
162
+ return None, None
163
+
164
+ for coll in ax.collections:
165
+ if "Contour" in type(coll).__name__:
166
+ return "contour", "contour"
167
+
168
+ return None, None
169
+
170
+
171
+ def _detect_bars(ax) -> Tuple[Optional[str], Optional[str]]:
172
+ """Detect bar and boxplots from containers."""
173
+ if len(ax.containers) == 0:
174
+ return None, None
175
+
176
+ if any("boxplot" in str(type(c)).lower() for c in ax.containers):
177
+ return "boxplot", "boxplot"
178
+
179
+ return "bar", "bar"
180
+
181
+
182
+ def _detect_patches(ax) -> Tuple[Optional[str], Optional[str]]:
183
+ """Detect histogram, violin, pie from patches."""
184
+ if len(ax.patches) == 0:
185
+ return None, None
186
+
187
+ # Check for pie chart (Wedge patches)
188
+ if any("Wedge" in type(p).__name__ for p in ax.patches):
189
+ return "pie", "pie"
190
+
191
+ # If there are many rectangular patches, likely histogram
192
+ if len(ax.patches) > 5:
193
+ return "hist", "hist"
194
+
195
+ # Check for violin plot
196
+ if any("Poly" in type(p).__name__ for p in ax.patches):
197
+ return "violin", "violinplot"
198
+
199
+ return None, None
200
+
201
+
202
+ def _detect_scatter(ax) -> Tuple[Optional[str], Optional[str]]:
203
+ """Detect scatter plots from PathCollection."""
204
+ if not hasattr(ax, "collections") or len(ax.collections) == 0:
205
+ return None, None
206
+
207
+ for coll in ax.collections:
208
+ if "PathCollection" in type(coll).__name__:
209
+ return "scatter", "scatter"
210
+
211
+ return None, None
212
+
213
+
214
+ def _detect_lines(ax) -> Tuple[Optional[str], Optional[str]]:
215
+ """Detect line and errorbar plots."""
216
+ if len(ax.lines) == 0:
217
+ return None, None
218
+
219
+ if any(hasattr(line, "_mpl_error") for line in ax.lines):
220
+ return "errorbar", "errorbar"
221
+
222
+ return "line", "plot"
223
+
224
+
225
+ # EOF
@@ -0,0 +1,127 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: "2026-01-24 (ywatanabe)"
3
+ # File: /home/ywatanabe/proj/scitex-python/src/scitex/plt/utils/_metadata/_legend.py
4
+
5
+ """
6
+ Legend extraction utilities for figure metadata.
7
+
8
+ Extracts legend information including handles and artist references.
9
+ """
10
+
11
+ from typing import Optional
12
+
13
+
14
+ def _extract_legend_info(ax) -> Optional[dict]:
15
+ """
16
+ Extract legend information from axes.
17
+
18
+ Uses matplotlib terminology for legend properties.
19
+
20
+ Parameters
21
+ ----------
22
+ ax : matplotlib.axes.Axes
23
+ The axes to extract legend from
24
+
25
+ Returns
26
+ -------
27
+ dict or None
28
+ Legend info dictionary with matplotlib properties, or None if no legend
29
+ """
30
+ legend = ax.get_legend()
31
+ if legend is None:
32
+ return None
33
+
34
+ legend_info = {
35
+ "visible": legend.get_visible(),
36
+ "loc": legend._loc if hasattr(legend, "_loc") else "best",
37
+ "frameon": legend.get_frame_on() if hasattr(legend, "get_frame_on") else True,
38
+ }
39
+
40
+ # ncol - number of columns
41
+ if hasattr(legend, "_ncols"):
42
+ legend_info["ncol"] = legend._ncols
43
+ elif hasattr(legend, "_ncol"):
44
+ legend_info["ncol"] = legend._ncol
45
+
46
+ # Extract legend handles with artist references
47
+ handles = _extract_legend_handles(ax, legend)
48
+ if handles:
49
+ legend_info["handles"] = handles
50
+
51
+ return legend_info
52
+
53
+
54
+ def _extract_legend_handles(ax, legend) -> list:
55
+ """Extract legend handles with artist references."""
56
+ handles = []
57
+ texts = legend.get_texts()
58
+ legend_handles = legend.legend_handles if hasattr(legend, "legend_handles") else []
59
+
60
+ # Get the raw matplotlib axes for accessing lines to match IDs
61
+ mpl_ax = ax._axis_mpl if hasattr(ax, "_axis_mpl") else ax
62
+
63
+ for i, text in enumerate(texts):
64
+ label_text = text.get_text()
65
+ handle_entry = {"label": label_text}
66
+
67
+ # Try to get artist_id from corresponding handle
68
+ artist_id = None
69
+ if i < len(legend_handles):
70
+ handle = legend_handles[i]
71
+ if hasattr(handle, "_scitex_id"):
72
+ artist_id = handle._scitex_id
73
+
74
+ # Fallback: find matching artist by label in axes artists
75
+ if artist_id is None:
76
+ artist_id = _find_artist_id_by_label(mpl_ax, label_text)
77
+
78
+ if artist_id:
79
+ handle_entry["artist_id"] = artist_id
80
+
81
+ handles.append(handle_entry)
82
+
83
+ return handles
84
+
85
+
86
+ def _find_artist_id_by_label(mpl_ax, label_text: str) -> Optional[str]:
87
+ """Find artist ID by matching label in axes artists."""
88
+ # Check lines
89
+ for line in mpl_ax.lines:
90
+ line_label = line.get_label()
91
+ if line_label == label_text:
92
+ if hasattr(line, "_scitex_id"):
93
+ return line._scitex_id
94
+ elif not line_label.startswith("_"):
95
+ return line_label
96
+
97
+ # Check collections (scatter)
98
+ for coll in mpl_ax.collections:
99
+ coll_label = coll.get_label() if hasattr(coll, "get_label") else ""
100
+ if coll_label == label_text:
101
+ if hasattr(coll, "_scitex_id"):
102
+ return coll._scitex_id
103
+ elif coll_label and not coll_label.startswith("_"):
104
+ return coll_label
105
+
106
+ # Check patches (bar/hist/pie)
107
+ for patch in mpl_ax.patches:
108
+ patch_label = patch.get_label() if hasattr(patch, "get_label") else ""
109
+ if patch_label == label_text:
110
+ if hasattr(patch, "_scitex_id"):
111
+ return patch._scitex_id
112
+ elif patch_label and not patch_label.startswith("_"):
113
+ return patch_label
114
+
115
+ # Check images (imshow)
116
+ for img in mpl_ax.images:
117
+ img_label = img.get_label() if hasattr(img, "get_label") else ""
118
+ if img_label == label_text:
119
+ if hasattr(img, "_scitex_id"):
120
+ return img._scitex_id
121
+ elif img_label and not img_label.startswith("_"):
122
+ return img_label
123
+
124
+ return None
125
+
126
+
127
+ # EOF
@@ -0,0 +1,117 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: "2026-01-24 (ywatanabe)"
3
+ # File: /home/ywatanabe/proj/scitex-python/src/scitex/plt/utils/_metadata/_rounding.py
4
+
5
+ """
6
+ Rounding utilities for figure metadata.
7
+
8
+ Provides precision-controlled rounding for various measurement types
9
+ (mm, inch, position, etc.) to ensure consistent JSON output.
10
+ """
11
+
12
+ from typing import List, Union
13
+
14
+ # Precision settings for JSON output
15
+ PRECISION = {
16
+ "mm": 2, # Millimeters: 0.01mm precision (10 microns)
17
+ "inch": 3, # Inches: 0.001 inch precision
18
+ "position": 3, # Normalized position: 0.001 precision
19
+ "lim": 2, # Axis limits: 2 decimal places
20
+ "linewidth": 2, # Line widths: 0.01 precision
21
+ }
22
+
23
+
24
+ class FixedFloat:
25
+ """
26
+ A float wrapper that preserves fixed decimal places in JSON output.
27
+
28
+ Example: FixedFloat(0.25, 3) -> "0.250" in JSON
29
+ """
30
+
31
+ def __init__(self, value: float, precision: int):
32
+ self.value = round(value, precision)
33
+ self.precision = precision
34
+
35
+ def __repr__(self):
36
+ return f"{self.value:.{self.precision}f}"
37
+
38
+ def __float__(self):
39
+ return self.value
40
+
41
+
42
+ def _round_value(
43
+ value: Union[float, int], precision: int, fixed: bool = False
44
+ ) -> Union[float, int, "FixedFloat"]:
45
+ """
46
+ Round a single value to specified precision.
47
+
48
+ Parameters
49
+ ----------
50
+ value : float or int
51
+ Value to round
52
+ precision : int
53
+ Number of decimal places
54
+ fixed : bool
55
+ If True, return FixedFloat with fixed decimal places (e.g., 0.250)
56
+ If False, return float (e.g., 0.25)
57
+ """
58
+ if isinstance(value, int):
59
+ if fixed:
60
+ return FixedFloat(float(value), precision)
61
+ return value
62
+ if isinstance(value, float):
63
+ if fixed:
64
+ return FixedFloat(value, precision)
65
+ return round(value, precision)
66
+ return value
67
+
68
+
69
+ def _round_list(values: List, precision: int, fixed: bool = False) -> List:
70
+ """Round all values in a list."""
71
+ return [_round_value(v, precision, fixed) for v in values]
72
+
73
+
74
+ def _round_dict(d: dict, precision_map: dict = None) -> dict:
75
+ """
76
+ Round all float values in a dict based on key-specific precision.
77
+
78
+ Parameters
79
+ ----------
80
+ d : dict
81
+ Dictionary to process
82
+ precision_map : dict, optional
83
+ Mapping of key patterns to precision values.
84
+ Default uses PRECISION settings based on key names.
85
+ """
86
+ if precision_map is None:
87
+ precision_map = {}
88
+
89
+ result = {}
90
+ for key, value in d.items():
91
+ # Determine precision based on key name
92
+ if "mm" in key.lower():
93
+ prec = PRECISION["mm"]
94
+ elif "inch" in key.lower():
95
+ prec = PRECISION["inch"]
96
+ elif "position" in key.lower() or key in ("left", "bottom", "right", "top"):
97
+ prec = PRECISION["position"]
98
+ elif "lim" in key.lower():
99
+ prec = PRECISION["lim"]
100
+ elif "width" in key.lower() and "line" in key.lower():
101
+ prec = PRECISION["linewidth"]
102
+ else:
103
+ prec = precision_map.get(key, 3) # Default 3 decimals
104
+
105
+ if isinstance(value, dict):
106
+ result[key] = _round_dict(value, precision_map)
107
+ elif isinstance(value, list):
108
+ result[key] = _round_list(value, prec)
109
+ elif isinstance(value, float):
110
+ result[key] = _round_value(value, prec)
111
+ else:
112
+ result[key] = value
113
+
114
+ return result
115
+
116
+
117
+ # EOF
@@ -0,0 +1,202 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: "2026-01-24 (ywatanabe)"
3
+ # File: /home/ywatanabe/proj/scitex-python/src/scitex/plt/utils/_metadata/_verification.py
4
+
5
+ """
6
+ CSV/JSON consistency verification for figure metadata.
7
+
8
+ Provides functions to verify that exported CSV data matches
9
+ the JSON metadata declarations.
10
+ """
11
+
12
+
13
+
14
+ def assert_csv_json_consistency(csv_path: str, json_path: str = None) -> None:
15
+ """
16
+ Assert that CSV data file and its JSON metadata are consistent.
17
+
18
+ Raises AssertionError if the column names don't match.
19
+
20
+ Parameters
21
+ ----------
22
+ csv_path : str
23
+ Path to the CSV data file
24
+ json_path : str, optional
25
+ Path to the JSON metadata file. If not provided, assumes
26
+ the JSON is at the same location with .json extension.
27
+
28
+ Raises
29
+ ------
30
+ AssertionError
31
+ If CSV and JSON column names don't match
32
+ FileNotFoundError
33
+ If CSV or JSON files don't exist
34
+
35
+ Examples
36
+ --------
37
+ >>> assert_csv_json_consistency('/tmp/plot.csv') # Passes silently if valid
38
+ >>> # Or use in tests:
39
+ >>> try:
40
+ ... assert_csv_json_consistency('/tmp/plot.csv')
41
+ ... except AssertionError as e:
42
+ ... print(f"Validation failed: {e}")
43
+ """
44
+ result = verify_csv_json_consistency(csv_path, json_path)
45
+
46
+ if result["errors"]:
47
+ raise FileNotFoundError("\n".join(result["errors"]))
48
+
49
+ if not result["valid"]:
50
+ msg_parts = ["CSV/JSON consistency check failed:"]
51
+ if result["missing_in_csv"]:
52
+ msg_parts.append(
53
+ f" columns_actual missing in CSV: {result['missing_in_csv']}"
54
+ )
55
+ if result["extra_in_csv"]:
56
+ msg_parts.append(f" Extra columns in CSV: {result['extra_in_csv']}")
57
+ if result.get("data_ref_missing"):
58
+ msg_parts.append(
59
+ f" data_ref columns missing in CSV: {result['data_ref_missing']}"
60
+ )
61
+ raise AssertionError("\n".join(msg_parts))
62
+
63
+
64
+ def verify_csv_json_consistency(csv_path: str, json_path: str = None) -> dict:
65
+ """
66
+ Verify consistency between CSV data file and its JSON metadata.
67
+
68
+ This function checks that:
69
+ 1. Column names in the CSV file match those declared in JSON's columns_actual
70
+ 2. Artist data_ref values in JSON match actual CSV column names
71
+
72
+ Parameters
73
+ ----------
74
+ csv_path : str
75
+ Path to the CSV data file
76
+ json_path : str, optional
77
+ Path to the JSON metadata file. If not provided, assumes
78
+ the JSON is at the same location with .json extension.
79
+
80
+ Returns
81
+ -------
82
+ dict
83
+ Verification result with keys:
84
+ - 'valid': bool - True if CSV and JSON are consistent
85
+ - 'csv_columns': list - Column names found in CSV
86
+ - 'json_columns': list - Column names declared in JSON
87
+ - 'data_ref_columns': list - Column names from artist data_ref
88
+ - 'missing_in_csv': list - Columns in JSON but not in CSV
89
+ - 'extra_in_csv': list - Columns in CSV but not in JSON
90
+ - 'data_ref_missing': list - data_ref columns not found in CSV
91
+ - 'errors': list - Any error messages
92
+
93
+ Examples
94
+ --------
95
+ >>> result = verify_csv_json_consistency('/tmp/plot.csv')
96
+ >>> print(result['valid'])
97
+ True
98
+ >>> print(result['missing_in_csv'])
99
+ []
100
+ """
101
+ import json
102
+ import os
103
+
104
+ import pandas as pd
105
+
106
+ result = {
107
+ "valid": False,
108
+ "csv_columns": [],
109
+ "json_columns": [],
110
+ "data_ref_columns": [],
111
+ "missing_in_csv": [],
112
+ "extra_in_csv": [],
113
+ "data_ref_missing": [],
114
+ "errors": [],
115
+ }
116
+
117
+ # Determine JSON path
118
+ if json_path is None:
119
+ base, _ = os.path.splitext(csv_path)
120
+ json_path = base + ".json"
121
+
122
+ # Check files exist
123
+ if not os.path.exists(csv_path):
124
+ result["errors"].append(f"CSV file not found: {csv_path}")
125
+ return result
126
+ if not os.path.exists(json_path):
127
+ result["errors"].append(f"JSON file not found: {json_path}")
128
+ return result
129
+
130
+ try:
131
+ # Read CSV columns
132
+ df = pd.read_csv(csv_path, nrows=0) # Just read header
133
+ csv_columns = list(df.columns)
134
+ result["csv_columns"] = csv_columns
135
+ except Exception as e:
136
+ result["errors"].append(f"Error reading CSV: {e}")
137
+ return result
138
+
139
+ try:
140
+ # Read JSON metadata
141
+ with open(json_path) as f:
142
+ metadata = json.load(f)
143
+
144
+ # Get columns_actual from data section
145
+ json_columns = []
146
+ if "data" in metadata and "columns_actual" in metadata["data"]:
147
+ json_columns = metadata["data"]["columns_actual"]
148
+ result["json_columns"] = json_columns
149
+
150
+ # Extract data_ref columns from artists
151
+ data_ref_columns = _extract_data_ref_columns(metadata)
152
+ result["data_ref_columns"] = data_ref_columns
153
+
154
+ except Exception as e:
155
+ result["errors"].append(f"Error reading JSON: {e}")
156
+ return result
157
+
158
+ # Compare columns_actual with CSV
159
+ csv_set = set(csv_columns)
160
+ json_set = set(json_columns)
161
+
162
+ result["missing_in_csv"] = list(json_set - csv_set)
163
+ result["extra_in_csv"] = list(csv_set - json_set)
164
+
165
+ # Check data_ref columns exist in CSV (if there are any)
166
+ if data_ref_columns:
167
+ data_ref_set = set(data_ref_columns)
168
+ result["data_ref_missing"] = list(data_ref_set - csv_set)
169
+
170
+ # Valid only if columns_actual matches AND data_ref columns are found
171
+ result["valid"] = (
172
+ len(result["missing_in_csv"]) == 0
173
+ and len(result["extra_in_csv"]) == 0
174
+ and len(result["data_ref_missing"]) == 0
175
+ )
176
+
177
+ return result
178
+
179
+
180
+ def _extract_data_ref_columns(metadata: dict) -> list:
181
+ """
182
+ Extract data_ref column names from metadata.
183
+
184
+ Skip 'derived_from' key as it contains descriptive text, not CSV column names.
185
+ Also skip 'row_index' as it's a numeric index, not a column name.
186
+ """
187
+ data_ref_columns = []
188
+ skip_keys = {"derived_from", "row_index"}
189
+
190
+ if "axes" in metadata:
191
+ for ax_key, ax_data in metadata["axes"].items():
192
+ if "artists" in ax_data:
193
+ for artist in ax_data["artists"]:
194
+ if "data_ref" in artist:
195
+ for key, val in artist["data_ref"].items():
196
+ if key not in skip_keys and isinstance(val, str):
197
+ data_ref_columns.append(val)
198
+
199
+ return data_ref_columns
200
+
201
+
202
+ # EOF
scitex/schema/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  <!-- ---
2
2
  !-- Timestamp: 2025-12-09 20:40:40
3
3
  !-- Author: ywatanabe
4
- !-- File: /home/ywatanabe/proj/scitex-code/src/scitex/schema/README.md
4
+ !-- File: /home/ywatanabe/proj/scitex-python/src/scitex/schema/README.md
5
5
  !-- --- -->
6
6
 
7
7
  # SciTeX Schema Module
@@ -71,6 +71,12 @@ try:
71
71
  except ImportError:
72
72
  utils = None
73
73
 
74
+ # CrossRef integration via crossref-local delegation (branded as crossref-scitex)
75
+ try:
76
+ from . import crossref_scitex
77
+ except ImportError:
78
+ crossref_scitex = None
79
+
74
80
  __all__ = [
75
81
  "ScholarConfig",
76
82
  "ScholarEngine",
@@ -81,6 +87,8 @@ __all__ = [
81
87
  "Papers",
82
88
  "Scholar",
83
89
  "utils",
90
+ # CrossRef integration (167M+ papers via crossref-local)
91
+ "crossref_scitex",
84
92
  ]
85
93
 
86
94
  # # Import core classes for advanced users