scitex 2.8.1__py3-none-any.whl → 2.10.0__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 (415) hide show
  1. scitex/__init__.py +15 -7
  2. scitex/__version__.py +1 -2
  3. scitex/_install_guide.py +250 -0
  4. scitex/_optional_deps.py +206 -39
  5. scitex/ai/_gen_ai/_Groq.py +2 -4
  6. scitex/ai/_gen_ai/_OpenAI.py +5 -2
  7. scitex/ai/_gen_ai/_Perplexity.py +20 -6
  8. scitex/audio/__init__.py +24 -15
  9. scitex/audio/_cross_process_lock.py +139 -0
  10. scitex/audio/_mcp_handlers.py +256 -0
  11. scitex/audio/_mcp_tool_schemas.py +203 -0
  12. scitex/audio/engines/elevenlabs_engine.py +5 -2
  13. scitex/audio/mcp_server.py +98 -457
  14. scitex/bridge/__init__.py +30 -19
  15. scitex/bridge/_figrecipe.py +245 -0
  16. scitex/bridge/_helpers.py +2 -1
  17. scitex/bridge/_plt_vis.py +23 -10
  18. scitex/bridge/_stats_plt.py +18 -5
  19. scitex/bridge/_stats_vis.py +16 -2
  20. scitex/browser/__init__.py +84 -44
  21. scitex/browser/automation/__init__.py +5 -1
  22. scitex/browser/core/BrowserMixin.py +17 -4
  23. scitex/browser/core/__init__.py +11 -2
  24. scitex/browser/remote/CaptchaHandler.py +1 -1
  25. scitex/browser/remote/ZenRowsAPIClient.py +1 -1
  26. scitex/capture/grid.py +487 -0
  27. scitex/capture/mcp_handlers.py +401 -0
  28. scitex/capture/mcp_tool_defs.py +192 -0
  29. scitex/capture/mcp_tools.py +241 -0
  30. scitex/capture/mcp_utils.py +30 -0
  31. scitex/cli/convert.py +421 -0
  32. scitex/cli/main.py +6 -4
  33. scitex/datetime/__init__.py +46 -0
  34. scitex/datetime/_linspace.py +100 -0
  35. scitex/datetime/_normalize_timestamp.py +306 -0
  36. scitex/db/_delete_duplicates.py +4 -4
  37. scitex/db/_sqlite3/_delete_duplicates.py +11 -2
  38. scitex/dev/plt/__init__.py +61 -62
  39. scitex/dev/plt/demo_plotters/__init__.py +0 -0
  40. scitex/dev/plt/demo_plotters/plot_mpl_axhline.py +28 -0
  41. scitex/dev/plt/demo_plotters/plot_mpl_axhspan.py +28 -0
  42. scitex/dev/plt/demo_plotters/plot_mpl_axvline.py +28 -0
  43. scitex/dev/plt/demo_plotters/plot_mpl_axvspan.py +28 -0
  44. scitex/dev/plt/demo_plotters/plot_mpl_bar.py +29 -0
  45. scitex/dev/plt/demo_plotters/plot_mpl_barh.py +29 -0
  46. scitex/dev/plt/demo_plotters/plot_mpl_boxplot.py +28 -0
  47. scitex/dev/plt/demo_plotters/plot_mpl_contour.py +31 -0
  48. scitex/dev/plt/demo_plotters/plot_mpl_contourf.py +31 -0
  49. scitex/dev/plt/demo_plotters/plot_mpl_errorbar.py +30 -0
  50. scitex/dev/plt/demo_plotters/plot_mpl_eventplot.py +28 -0
  51. scitex/dev/plt/demo_plotters/plot_mpl_fill.py +30 -0
  52. scitex/dev/plt/demo_plotters/plot_mpl_fill_between.py +31 -0
  53. scitex/dev/plt/demo_plotters/plot_mpl_hexbin.py +28 -0
  54. scitex/dev/plt/demo_plotters/plot_mpl_hist.py +28 -0
  55. scitex/dev/plt/demo_plotters/plot_mpl_hist2d.py +28 -0
  56. scitex/dev/plt/demo_plotters/plot_mpl_imshow.py +29 -0
  57. scitex/dev/plt/demo_plotters/plot_mpl_pcolormesh.py +31 -0
  58. scitex/dev/plt/demo_plotters/plot_mpl_pie.py +29 -0
  59. scitex/dev/plt/demo_plotters/plot_mpl_plot.py +29 -0
  60. scitex/dev/plt/demo_plotters/plot_mpl_quiver.py +31 -0
  61. scitex/dev/plt/demo_plotters/plot_mpl_scatter.py +28 -0
  62. scitex/dev/plt/demo_plotters/plot_mpl_stackplot.py +31 -0
  63. scitex/dev/plt/demo_plotters/plot_mpl_stem.py +29 -0
  64. scitex/dev/plt/demo_plotters/plot_mpl_step.py +29 -0
  65. scitex/dev/plt/demo_plotters/plot_mpl_violinplot.py +28 -0
  66. scitex/dev/plt/demo_plotters/plot_sns_barplot.py +29 -0
  67. scitex/dev/plt/demo_plotters/plot_sns_boxplot.py +29 -0
  68. scitex/dev/plt/demo_plotters/plot_sns_heatmap.py +28 -0
  69. scitex/dev/plt/demo_plotters/plot_sns_histplot.py +29 -0
  70. scitex/dev/plt/demo_plotters/plot_sns_kdeplot.py +29 -0
  71. scitex/dev/plt/demo_plotters/plot_sns_lineplot.py +31 -0
  72. scitex/dev/plt/demo_plotters/plot_sns_scatterplot.py +29 -0
  73. scitex/dev/plt/demo_plotters/plot_sns_stripplot.py +29 -0
  74. scitex/dev/plt/demo_plotters/plot_sns_swarmplot.py +29 -0
  75. scitex/dev/plt/demo_plotters/plot_sns_violinplot.py +29 -0
  76. scitex/dev/plt/demo_plotters/plot_stx_bar.py +29 -0
  77. scitex/dev/plt/demo_plotters/plot_stx_barh.py +29 -0
  78. scitex/dev/plt/demo_plotters/plot_stx_box.py +28 -0
  79. scitex/dev/plt/demo_plotters/plot_stx_boxplot.py +28 -0
  80. scitex/dev/plt/demo_plotters/plot_stx_conf_mat.py +28 -0
  81. scitex/dev/plt/demo_plotters/plot_stx_contour.py +31 -0
  82. scitex/dev/plt/demo_plotters/plot_stx_ecdf.py +28 -0
  83. scitex/dev/plt/demo_plotters/plot_stx_errorbar.py +30 -0
  84. scitex/dev/plt/demo_plotters/plot_stx_fill_between.py +31 -0
  85. scitex/dev/plt/demo_plotters/plot_stx_fillv.py +28 -0
  86. scitex/dev/plt/demo_plotters/plot_stx_heatmap.py +28 -0
  87. scitex/dev/plt/demo_plotters/plot_stx_image.py +28 -0
  88. scitex/dev/plt/demo_plotters/plot_stx_imshow.py +28 -0
  89. scitex/dev/plt/demo_plotters/plot_stx_joyplot.py +28 -0
  90. scitex/dev/plt/demo_plotters/plot_stx_kde.py +28 -0
  91. scitex/dev/plt/demo_plotters/plot_stx_line.py +28 -0
  92. scitex/dev/plt/demo_plotters/plot_stx_mean_ci.py +28 -0
  93. scitex/dev/plt/demo_plotters/plot_stx_mean_std.py +28 -0
  94. scitex/dev/plt/demo_plotters/plot_stx_median_iqr.py +28 -0
  95. scitex/dev/plt/demo_plotters/plot_stx_raster.py +28 -0
  96. scitex/dev/plt/demo_plotters/plot_stx_rectangle.py +28 -0
  97. scitex/dev/plt/demo_plotters/plot_stx_scatter.py +29 -0
  98. scitex/dev/plt/demo_plotters/plot_stx_shaded_line.py +29 -0
  99. scitex/dev/plt/demo_plotters/plot_stx_violin.py +28 -0
  100. scitex/dev/plt/demo_plotters/plot_stx_violinplot.py +28 -0
  101. scitex/dev/plt/mpl/get_dir_ax.py +46 -0
  102. scitex/dev/plt/mpl/get_signatures.py +176 -0
  103. scitex/dev/plt/mpl/get_signatures_details.py +522 -0
  104. scitex/dict/_pop_keys.py +1 -7
  105. scitex/dsp/__init__.py +15 -10
  106. scitex/dsp/add_noise.py +5 -2
  107. scitex/dsp/example.py +35 -22
  108. scitex/dsp/filt.py +8 -3
  109. scitex/dsp/reference.py +3 -2
  110. scitex/dsp/utils/__init__.py +2 -1
  111. scitex/dsp/utils/_differential_bandpass_filters.py +14 -4
  112. scitex/dt/__init__.py +39 -2
  113. scitex/errors.py +82 -521
  114. scitex/fig/__init__.py +4 -4
  115. scitex/fig/editor/edit/panel_loader.py +1 -1
  116. scitex/fig/io/_bundle.py +7 -7
  117. scitex/fts/README.md +262 -0
  118. scitex/fts/TODO.md +66 -0
  119. scitex/fts/__init__.py +90 -0
  120. scitex/fts/_bundle/README_IN_BUNDLE.md +102 -0
  121. scitex/fts/_bundle/_FTS.py +657 -0
  122. scitex/fts/_bundle/__init__.py +38 -0
  123. scitex/fts/_bundle/_children.py +216 -0
  124. scitex/fts/_bundle/_conversion/__init__.py +15 -0
  125. scitex/fts/_bundle/_conversion/_bundle2dict.py +44 -0
  126. scitex/fts/_bundle/_conversion/_dict2bundle.py +50 -0
  127. scitex/fts/_bundle/_dataclasses/_Axes.py +57 -0
  128. scitex/fts/_bundle/_dataclasses/_BBox.py +54 -0
  129. scitex/fts/_bundle/_dataclasses/_ColumnDef.py +72 -0
  130. scitex/fts/_bundle/_dataclasses/_DataFormat.py +40 -0
  131. scitex/fts/_bundle/_dataclasses/_DataInfo.py +135 -0
  132. scitex/fts/_bundle/_dataclasses/_DataSource.py +44 -0
  133. scitex/fts/_bundle/_dataclasses/_Node.py +319 -0
  134. scitex/fts/_bundle/_dataclasses/_NodeRefs.py +45 -0
  135. scitex/fts/_bundle/_dataclasses/_SizeMM.py +38 -0
  136. scitex/fts/_bundle/_dataclasses/__init__.py +35 -0
  137. scitex/fts/_bundle/_extractors/__init__.py +32 -0
  138. scitex/fts/_bundle/_extractors/_extract_bar.py +131 -0
  139. scitex/fts/_bundle/_extractors/_extract_line.py +71 -0
  140. scitex/fts/_bundle/_extractors/_extract_scatter.py +79 -0
  141. scitex/fts/_bundle/_loader.py +134 -0
  142. scitex/fts/_bundle/_mpl_helpers.py +389 -0
  143. scitex/fts/_bundle/_saver.py +269 -0
  144. scitex/fts/_bundle/_storage.py +200 -0
  145. scitex/fts/_bundle/_utils/__init__.py +55 -0
  146. scitex/fts/_bundle/_utils/_const.py +26 -0
  147. scitex/fts/_bundle/_utils/_errors.py +73 -0
  148. scitex/fts/_bundle/_utils/_generate.py +21 -0
  149. scitex/fts/_bundle/_utils/_types.py +76 -0
  150. scitex/fts/_bundle/_validation.py +434 -0
  151. scitex/fts/_bundle/_zipbundle.py +165 -0
  152. scitex/fts/_fig/__init__.py +22 -0
  153. scitex/fts/_fig/_backend/__init__.py +53 -0
  154. scitex/fts/_fig/_backend/_export.py +165 -0
  155. scitex/fts/_fig/_backend/_parser.py +188 -0
  156. scitex/fts/_fig/_backend/_render.py +538 -0
  157. scitex/fts/_fig/_composite.py +345 -0
  158. scitex/fts/_fig/_dataclasses/_ChannelEncoding.py +46 -0
  159. scitex/fts/_fig/_dataclasses/_Encoding.py +82 -0
  160. scitex/fts/_fig/_dataclasses/_Theme.py +441 -0
  161. scitex/fts/_fig/_dataclasses/_TraceEncoding.py +52 -0
  162. scitex/fts/_fig/_dataclasses/__init__.py +47 -0
  163. scitex/fts/_fig/_editor/__init__.py +14 -0
  164. scitex/fts/_fig/_editor/_cui/__init__.py +33 -0
  165. scitex/fts/_fig/_editor/_cui/_backend_detector.py +39 -0
  166. scitex/fts/_fig/_editor/_cui/_bundle_resolver.py +366 -0
  167. scitex/fts/_fig/_editor/_cui/_editor_launcher.py +175 -0
  168. scitex/fts/_fig/_editor/_cui/_manual_handler.py +52 -0
  169. scitex/fts/_fig/_editor/_cui/_panel_loader.py +246 -0
  170. scitex/fts/_fig/_editor/_cui/_path_resolver.py +66 -0
  171. scitex/fts/_fig/_editor/_defaults.py +300 -0
  172. scitex/fts/_fig/_editor/_gui/__init__.py +11 -0
  173. scitex/fts/_fig/_editor/_gui/_flask_editor/__init__.py +20 -0
  174. scitex/fts/_fig/_editor/_gui/_flask_editor/_bbox.py +1339 -0
  175. scitex/fts/_fig/_editor/_gui/_flask_editor/_core.py +1688 -0
  176. scitex/fts/_fig/_editor/_gui/_flask_editor/_plotter.py +664 -0
  177. scitex/fts/_fig/_editor/_gui/_flask_editor/_renderer.py +853 -0
  178. scitex/fts/_fig/_editor/_gui/_flask_editor/_utils.py +79 -0
  179. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/base/reset.css +41 -0
  180. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/base/typography.css +16 -0
  181. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/base/variables.css +85 -0
  182. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/components/buttons.css +217 -0
  183. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/components/context-menu.css +93 -0
  184. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/components/dropdown.css +57 -0
  185. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/components/forms.css +112 -0
  186. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/components/modal.css +59 -0
  187. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/components/sections.css +212 -0
  188. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/features/canvas.css +176 -0
  189. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/features/element-inspector.css +190 -0
  190. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/features/loading.css +59 -0
  191. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/features/overlay.css +45 -0
  192. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/features/panel-grid.css +95 -0
  193. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/features/selection.css +101 -0
  194. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/features/statistics.css +138 -0
  195. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/index.css +31 -0
  196. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/layout/container.css +7 -0
  197. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/layout/controls.css +56 -0
  198. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/layout/preview.css +78 -0
  199. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/alignment/axis.js +314 -0
  200. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/alignment/basic.js +107 -0
  201. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/alignment/distribute.js +54 -0
  202. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/canvas/canvas.js +172 -0
  203. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/canvas/dragging.js +258 -0
  204. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/canvas/resize.js +48 -0
  205. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/canvas/selection.js +71 -0
  206. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/core/api.js +288 -0
  207. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/core/state.js +143 -0
  208. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/core/utils.js +245 -0
  209. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/dev/element-inspector.js +992 -0
  210. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/editor/bbox.js +339 -0
  211. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/editor/element-drag.js +286 -0
  212. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/editor/overlay.js +371 -0
  213. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/editor/preview.js +293 -0
  214. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/main.js +426 -0
  215. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/shortcuts/context-menu.js +152 -0
  216. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/shortcuts/keyboard.js +265 -0
  217. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/ui/controls.js +184 -0
  218. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/ui/download.js +57 -0
  219. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/ui/help.js +100 -0
  220. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/ui/theme.js +34 -0
  221. scitex/fts/_fig/_editor/_gui/_flask_editor/templates/__init__.py +124 -0
  222. scitex/fts/_fig/_editor/_gui/_flask_editor/templates/_html.py +851 -0
  223. scitex/fts/_fig/_editor/_gui/_flask_editor/templates/_scripts.py +4932 -0
  224. scitex/fts/_fig/_editor/_gui/_flask_editor/templates/_styles.py +1657 -0
  225. scitex/fts/_fig/_editor/_gui/_flask_editor.py +36 -0
  226. scitex/fts/_fig/_models/_Annotations.py +115 -0
  227. scitex/fts/_fig/_models/_Axes.py +152 -0
  228. scitex/fts/_fig/_models/_Figure.py +138 -0
  229. scitex/fts/_fig/_models/_Guides.py +104 -0
  230. scitex/fts/_fig/_models/_Plot.py +123 -0
  231. scitex/fts/_fig/_models/_Styles.py +245 -0
  232. scitex/fts/_fig/_models/__init__.py +80 -0
  233. scitex/fts/_fig/_models/_plot_types/__init__.py +156 -0
  234. scitex/fts/_fig/_models/_plot_types/_bar.py +43 -0
  235. scitex/fts/_fig/_models/_plot_types/_box.py +38 -0
  236. scitex/fts/_fig/_models/_plot_types/_distribution.py +36 -0
  237. scitex/fts/_fig/_models/_plot_types/_errorbar.py +60 -0
  238. scitex/fts/_fig/_models/_plot_types/_histogram.py +30 -0
  239. scitex/fts/_fig/_models/_plot_types/_image.py +61 -0
  240. scitex/fts/_fig/_models/_plot_types/_line.py +57 -0
  241. scitex/fts/_fig/_models/_plot_types/_scatter.py +30 -0
  242. scitex/fts/_fig/_models/_plot_types/_seaborn.py +121 -0
  243. scitex/fts/_fig/_models/_plot_types/_violin.py +36 -0
  244. scitex/fts/_fig/_utils/__init__.py +129 -0
  245. scitex/fts/_fig/_utils/_auto_layout.py +127 -0
  246. scitex/fts/_fig/_utils/_calc_bounds.py +111 -0
  247. scitex/fts/_fig/_utils/_const_sizes.py +48 -0
  248. scitex/fts/_fig/_utils/_convert_coords.py +77 -0
  249. scitex/fts/_fig/_utils/_get_template.py +178 -0
  250. scitex/fts/_fig/_utils/_normalize.py +73 -0
  251. scitex/fts/_fig/_utils/_plot_layout.py +397 -0
  252. scitex/fts/_fig/_utils/_validate.py +197 -0
  253. scitex/fts/_kinds/__init__.py +45 -0
  254. scitex/fts/_kinds/_figure/__init__.py +19 -0
  255. scitex/fts/_kinds/_figure/_composite.py +345 -0
  256. scitex/fts/_kinds/_plot/__init__.py +25 -0
  257. scitex/fts/_kinds/_plot/_backend/__init__.py +53 -0
  258. scitex/fts/_kinds/_plot/_backend/_export.py +165 -0
  259. scitex/fts/_kinds/_plot/_backend/_parser.py +188 -0
  260. scitex/fts/_kinds/_plot/_backend/_render.py +538 -0
  261. scitex/fts/_kinds/_plot/_dataclasses/_ChannelEncoding.py +46 -0
  262. scitex/fts/_kinds/_plot/_dataclasses/_Encoding.py +82 -0
  263. scitex/fts/_kinds/_plot/_dataclasses/_Theme.py +441 -0
  264. scitex/fts/_kinds/_plot/_dataclasses/_TraceEncoding.py +52 -0
  265. scitex/fts/_kinds/_plot/_dataclasses/__init__.py +47 -0
  266. scitex/fts/_kinds/_plot/_models/_Annotations.py +115 -0
  267. scitex/fts/_kinds/_plot/_models/_Axes.py +152 -0
  268. scitex/fts/_kinds/_plot/_models/_Figure.py +138 -0
  269. scitex/fts/_kinds/_plot/_models/_Guides.py +104 -0
  270. scitex/fts/_kinds/_plot/_models/_Plot.py +123 -0
  271. scitex/fts/_kinds/_plot/_models/_Styles.py +245 -0
  272. scitex/fts/_kinds/_plot/_models/__init__.py +80 -0
  273. scitex/fts/_kinds/_plot/_models/_plot_types/__init__.py +156 -0
  274. scitex/fts/_kinds/_plot/_models/_plot_types/_bar.py +43 -0
  275. scitex/fts/_kinds/_plot/_models/_plot_types/_box.py +38 -0
  276. scitex/fts/_kinds/_plot/_models/_plot_types/_distribution.py +36 -0
  277. scitex/fts/_kinds/_plot/_models/_plot_types/_errorbar.py +60 -0
  278. scitex/fts/_kinds/_plot/_models/_plot_types/_histogram.py +30 -0
  279. scitex/fts/_kinds/_plot/_models/_plot_types/_image.py +61 -0
  280. scitex/fts/_kinds/_plot/_models/_plot_types/_line.py +57 -0
  281. scitex/fts/_kinds/_plot/_models/_plot_types/_scatter.py +30 -0
  282. scitex/fts/_kinds/_plot/_models/_plot_types/_seaborn.py +121 -0
  283. scitex/fts/_kinds/_plot/_models/_plot_types/_violin.py +36 -0
  284. scitex/fts/_kinds/_plot/_utils/__init__.py +129 -0
  285. scitex/fts/_kinds/_plot/_utils/_auto_layout.py +127 -0
  286. scitex/fts/_kinds/_plot/_utils/_calc_bounds.py +111 -0
  287. scitex/fts/_kinds/_plot/_utils/_const_sizes.py +48 -0
  288. scitex/fts/_kinds/_plot/_utils/_convert_coords.py +77 -0
  289. scitex/fts/_kinds/_plot/_utils/_get_template.py +178 -0
  290. scitex/fts/_kinds/_plot/_utils/_normalize.py +73 -0
  291. scitex/fts/_kinds/_plot/_utils/_plot_layout.py +397 -0
  292. scitex/fts/_kinds/_plot/_utils/_validate.py +197 -0
  293. scitex/fts/_kinds/_shape/__init__.py +141 -0
  294. scitex/fts/_kinds/_stats/__init__.py +56 -0
  295. scitex/fts/_kinds/_stats/_dataclasses/_Stats.py +423 -0
  296. scitex/fts/_kinds/_stats/_dataclasses/__init__.py +48 -0
  297. scitex/fts/_kinds/_table/__init__.py +72 -0
  298. scitex/fts/_kinds/_table/_latex/__init__.py +93 -0
  299. scitex/fts/_kinds/_table/_latex/_editor/__init__.py +11 -0
  300. scitex/fts/_kinds/_table/_latex/_editor/_app.py +725 -0
  301. scitex/fts/_kinds/_table/_latex/_export.py +279 -0
  302. scitex/fts/_kinds/_table/_latex/_figure_exporter.py +153 -0
  303. scitex/fts/_kinds/_table/_latex/_stats_formatter.py +274 -0
  304. scitex/fts/_kinds/_table/_latex/_table_exporter.py +362 -0
  305. scitex/fts/_kinds/_table/_latex/_utils.py +369 -0
  306. scitex/fts/_kinds/_table/_latex/_validator.py +445 -0
  307. scitex/fts/_kinds/_text/__init__.py +77 -0
  308. scitex/fts/_schemas/data_info.schema.json +75 -0
  309. scitex/fts/_schemas/encoding.schema.json +90 -0
  310. scitex/fts/_schemas/node.schema.json +145 -0
  311. scitex/fts/_schemas/render_manifest.schema.json +62 -0
  312. scitex/fts/_schemas/stats.schema.json +132 -0
  313. scitex/fts/_schemas/theme.schema.json +141 -0
  314. scitex/fts/_stats/__init__.py +48 -0
  315. scitex/fts/_stats/_dataclasses/_Stats.py +423 -0
  316. scitex/fts/_stats/_dataclasses/__init__.py +48 -0
  317. scitex/fts/_tables/__init__.py +65 -0
  318. scitex/fts/_tables/_latex/__init__.py +93 -0
  319. scitex/fts/_tables/_latex/_editor/__init__.py +11 -0
  320. scitex/fts/_tables/_latex/_editor/_app.py +725 -0
  321. scitex/fts/_tables/_latex/_export.py +279 -0
  322. scitex/fts/_tables/_latex/_figure_exporter.py +153 -0
  323. scitex/fts/_tables/_latex/_stats_formatter.py +274 -0
  324. scitex/fts/_tables/_latex/_table_exporter.py +362 -0
  325. scitex/fts/_tables/_latex/_utils.py +369 -0
  326. scitex/fts/_tables/_latex/_validator.py +445 -0
  327. scitex/gen/__init__.py +66 -25
  328. scitex/gen/misc.py +28 -0
  329. scitex/io/__init__.py +47 -32
  330. scitex/io/_load.py +87 -36
  331. scitex/io/_load_modules/__init__.py +10 -7
  332. scitex/io/_load_modules/_pandas.py +6 -1
  333. scitex/io/_save.py +299 -1556
  334. scitex/io/_save_modules/__init__.py +76 -19
  335. scitex/io/_save_modules/_figure_utils.py +90 -0
  336. scitex/io/_save_modules/_image_csv.py +497 -0
  337. scitex/io/_save_modules/_legends.py +91 -0
  338. scitex/io/_save_modules/_pltz_bundle.py +356 -0
  339. scitex/io/_save_modules/_pltz_stx.py +536 -0
  340. scitex/io/_save_modules/_stx_bundle.py +104 -0
  341. scitex/io/_save_modules/_symlink.py +96 -0
  342. scitex/io/_save_modules/_yaml.py +1 -1
  343. scitex/io/_save_modules/_zarr.py +64 -18
  344. scitex/io/bundle/README.md +212 -0
  345. scitex/io/bundle/__init__.py +110 -0
  346. scitex/io/{_bundle.py → bundle/_core.py} +168 -97
  347. scitex/io/bundle/_nested.py +713 -0
  348. scitex/io/bundle/_types.py +74 -0
  349. scitex/io/{_zip_bundle.py → bundle/_zip.py} +93 -45
  350. scitex/io/utils/h5_to_zarr.py +1 -1
  351. scitex/logging/__init__.py +108 -13
  352. scitex/logging/_errors.py +508 -0
  353. scitex/logging/_formatters.py +30 -6
  354. scitex/logging/_warnings.py +261 -0
  355. scitex/plt/__init__.py +4 -1
  356. scitex/plt/_figrecipe.py +236 -0
  357. scitex/plt/_subplots/_AxisWrapper.py +6 -0
  358. scitex/plt/_subplots/_AxisWrapperMixins/_UnitAwareMixin.py +112 -1
  359. scitex/plt/_subplots/_FigWrapper.py +15 -0
  360. scitex/plt/_subplots/_SubplotsWrapper.py +125 -489
  361. scitex/plt/_subplots/_export_as_csv.py +11 -0
  362. scitex/plt/_subplots/_export_as_csv_formatters/__init__.py +2 -0
  363. scitex/plt/_subplots/_export_as_csv_formatters/_format_pcolormesh.py +66 -0
  364. scitex/plt/_subplots/_export_as_csv_formatters/_format_stackplot.py +62 -0
  365. scitex/plt/_subplots/_export_as_csv_formatters/test_formatters.py +208 -0
  366. scitex/plt/_subplots/_fonts.py +71 -0
  367. scitex/plt/_subplots/_mm_layout.py +282 -0
  368. scitex/plt/gallery/__init__.py +99 -2
  369. scitex/plt/styles/_plot_postprocess.py +3 -1
  370. scitex/plt/utils/_configure_mpl.py +16 -19
  371. scitex/repro/_RandomStateManager.py +13 -8
  372. scitex/resource/__init__.py +19 -1
  373. scitex/resource/_utils/_get_env_info.py +13 -25
  374. scitex/schema/__init__.py +149 -160
  375. scitex/schema/_encoding.py +273 -0
  376. scitex/schema/_figure_elements.py +406 -0
  377. scitex/schema/_theme.py +360 -0
  378. scitex/schema/_validation.py +0 -98
  379. scitex/scholar/__init__.py +56 -14
  380. scitex/scholar/auth/ScholarAuthManager.py +1 -1
  381. scitex/scholar/auth/__init__.py +11 -2
  382. scitex/scholar/auth/providers/BaseAuthenticator.py +1 -1
  383. scitex/scholar/auth/providers/EZProxyAuthenticator.py +1 -1
  384. scitex/scholar/auth/providers/OpenAthensAuthenticator.py +1 -1
  385. scitex/scholar/auth/providers/ShibbolethAuthenticator.py +1 -1
  386. scitex/scholar/config/ScholarConfig.py +1 -1
  387. scitex/scholar/core/Scholar.py +1 -1
  388. scitex/session/_decorator.py +18 -16
  389. scitex/session/_lifecycle.py +9 -11
  390. scitex/session/template.py +9 -8
  391. scitex/sh/test_sh.py +72 -0
  392. scitex/sh/test_sh_simple.py +61 -0
  393. scitex/stats/__init__.py +221 -97
  394. scitex/stats/_schema.py +21 -22
  395. scitex/stats/descriptive/_circular.py +212 -351
  396. scitex/stats/descriptive/_describe.py +81 -132
  397. scitex/stats/descriptive/_nan.py +205 -433
  398. scitex/stats/descriptive/_real.py +127 -141
  399. scitex/str/_format_plot_text.py +5 -5
  400. scitex/str/_latex.py +26 -84
  401. scitex/str/_latex_fallback.py +53 -47
  402. scitex/web/_search_pubmed.py +5 -4
  403. scitex/writer/tests/test_diff_between.py +451 -0
  404. scitex/writer/tests/test_document_section.py +311 -0
  405. scitex/writer/tests/test_document_workflow.py +393 -0
  406. scitex/writer/tests/test_writer.py +361 -0
  407. scitex/writer/tests/test_writer_integration.py +303 -0
  408. {scitex-2.8.1.dist-info → scitex-2.10.0.dist-info}/METADATA +364 -181
  409. {scitex-2.8.1.dist-info → scitex-2.10.0.dist-info}/RECORD +412 -97
  410. scitex/scholar/docs/to_claude/guidelines/examples/mgmt/ARCHITECTURE_EXAMPLE.md +0 -905
  411. scitex/scholar/docs/to_claude/guidelines/examples/mgmt/BULLETIN_BOARD_EXAMPLE.md +0 -99
  412. scitex/scholar/docs/to_claude/guidelines/examples/mgmt/PROJECT_DESCRIPTION_EXAMPLE.md +0 -96
  413. {scitex-2.8.1.dist-info → scitex-2.10.0.dist-info}/WHEEL +0 -0
  414. {scitex-2.8.1.dist-info → scitex-2.10.0.dist-info}/entry_points.txt +0 -0
  415. {scitex-2.8.1.dist-info → scitex-2.10.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: 2025-12-20
3
+ # File: /home/ywatanabe/proj/scitex-code/src/scitex/fts/_bundle/_extractors/_extract_line.py
4
+
5
+ """Line plot data and encoding extraction."""
6
+
7
+ from typing import TYPE_CHECKING, Dict, List, Tuple
8
+
9
+ import numpy as np
10
+
11
+ if TYPE_CHECKING:
12
+ from matplotlib.axes import Axes
13
+
14
+ from ..._fig._dataclasses import TraceEncoding
15
+
16
+
17
+ def extract_line_data(ax: "Axes", ax_idx: int) -> Dict[str, np.ndarray]:
18
+ """Extract line plot data from axes.
19
+
20
+ Args:
21
+ ax: Matplotlib axes
22
+ ax_idx: Axes index for column naming
23
+
24
+ Returns:
25
+ Dict mapping column names to data arrays
26
+ """
27
+ data = {}
28
+ for line_idx, line in enumerate(ax.get_lines()):
29
+ label = line.get_label()
30
+ if label is None or label.startswith("_"):
31
+ label = f"series_{line_idx}"
32
+
33
+ xdata, ydata = line.get_data()
34
+ if len(xdata) > 0:
35
+ x_col = f"ax{ax_idx}_line{line_idx}_x"
36
+ y_col = f"ax{ax_idx}_line{line_idx}_y"
37
+ data[x_col] = np.array(xdata, dtype=float)
38
+ data[y_col] = np.array(ydata, dtype=float)
39
+
40
+ return data
41
+
42
+
43
+ def build_line_traces(ax: "Axes", ax_idx: int) -> List["TraceEncoding"]:
44
+ """Build encoding traces for line plots.
45
+
46
+ Args:
47
+ ax: Matplotlib axes
48
+ ax_idx: Axes index for trace ID
49
+
50
+ Returns:
51
+ List of TraceEncoding objects
52
+ """
53
+ from ..._fig._dataclasses import ChannelEncoding, TraceEncoding
54
+
55
+ traces = []
56
+ for line_idx, line in enumerate(ax.get_lines()):
57
+ label = line.get_label()
58
+ if label and not label.startswith("_"):
59
+ trace = TraceEncoding(
60
+ trace_id=f"line_{ax_idx}_{line_idx}",
61
+ x=ChannelEncoding(column=f"ax{ax_idx}_line{line_idx}_x"),
62
+ y=ChannelEncoding(column=f"ax{ax_idx}_line{line_idx}_y"),
63
+ )
64
+ traces.append(trace)
65
+
66
+ return traces
67
+
68
+
69
+ __all__ = ["extract_line_data", "build_line_traces"]
70
+
71
+ # EOF
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: 2025-12-20
3
+ # File: /home/ywatanabe/proj/scitex-code/src/scitex/fts/_bundle/_extractors/_extract_scatter.py
4
+
5
+ """Scatter plot data and encoding extraction."""
6
+
7
+ from typing import TYPE_CHECKING, Dict, List, Tuple
8
+
9
+ import numpy as np
10
+
11
+ if TYPE_CHECKING:
12
+ from matplotlib.axes import Axes
13
+
14
+ from ..._fig._dataclasses import TraceEncoding
15
+
16
+
17
+ def extract_scatter_data(ax: "Axes", ax_idx: int) -> Dict[str, np.ndarray]:
18
+ """Extract scatter plot data from axes (PathCollection).
19
+
20
+ Args:
21
+ ax: Matplotlib axes
22
+ ax_idx: Axes index for column naming
23
+
24
+ Returns:
25
+ Dict mapping column names to data arrays
26
+ """
27
+ from matplotlib.collections import PathCollection
28
+
29
+ data = {}
30
+ scatter_idx = 0
31
+
32
+ for child in ax.get_children():
33
+ if isinstance(child, PathCollection):
34
+ offsets = child.get_offsets()
35
+ if len(offsets) > 0:
36
+ x_col = f"ax{ax_idx}_scatter{scatter_idx}_x"
37
+ y_col = f"ax{ax_idx}_scatter{scatter_idx}_y"
38
+ data[x_col] = np.array(offsets[:, 0], dtype=float)
39
+ data[y_col] = np.array(offsets[:, 1], dtype=float)
40
+ scatter_idx += 1
41
+
42
+ return data
43
+
44
+
45
+ def build_scatter_traces(ax: "Axes", ax_idx: int) -> List["TraceEncoding"]:
46
+ """Build encoding traces for scatter plots.
47
+
48
+ Args:
49
+ ax: Matplotlib axes
50
+ ax_idx: Axes index for trace ID
51
+
52
+ Returns:
53
+ List of TraceEncoding objects
54
+ """
55
+ from matplotlib.collections import PathCollection
56
+
57
+ from ..._fig._dataclasses import ChannelEncoding, TraceEncoding
58
+
59
+ traces = []
60
+ scatter_idx = 0
61
+
62
+ for child in ax.get_children():
63
+ if isinstance(child, PathCollection):
64
+ offsets = child.get_offsets()
65
+ if len(offsets) > 0:
66
+ trace = TraceEncoding(
67
+ trace_id=f"scatter_{ax_idx}_{scatter_idx}",
68
+ x=ChannelEncoding(column=f"ax{ax_idx}_scatter{scatter_idx}_x"),
69
+ y=ChannelEncoding(column=f"ax{ax_idx}_scatter{scatter_idx}_y"),
70
+ )
71
+ traces.append(trace)
72
+ scatter_idx += 1
73
+
74
+ return traces
75
+
76
+
77
+ __all__ = ["extract_scatter_data", "build_scatter_traces"]
78
+
79
+ # EOF
@@ -0,0 +1,134 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: 2025-12-20
3
+ # File: /home/ywatanabe/proj/scitex-code/src/scitex/fts/_bundle/_loader.py
4
+
5
+ """FTS Bundle loading utilities.
6
+
7
+ Loads bundles using the new canonical/artifacts/payload/children structure.
8
+ Supports backwards compatibility with old flat structure (node.json at root).
9
+
10
+ New structure:
11
+ canonical/spec.json (was node.json)
12
+ canonical/encoding.json (was encoding.json)
13
+ canonical/theme.json (was theme.json)
14
+ canonical/data_info.json (was data/data_info.json)
15
+ payload/stats.json (was stats/stats.json)
16
+ """
17
+
18
+ from pathlib import Path
19
+ from typing import TYPE_CHECKING, Optional, Tuple
20
+
21
+ from ._storage import get_storage
22
+
23
+ if TYPE_CHECKING:
24
+ from ._dataclasses import DataInfo, Node
25
+ from .._fig import Encoding, Theme
26
+ from .._stats import Stats
27
+
28
+
29
+ def load_bundle_components(
30
+ path: Path,
31
+ ) -> Tuple[
32
+ Optional["Node"],
33
+ Optional["Encoding"],
34
+ Optional["Theme"],
35
+ Optional["Stats"],
36
+ Optional["DataInfo"],
37
+ ]:
38
+ """Load all bundle components from storage.
39
+
40
+ Supports both new canonical/ structure and legacy flat structure.
41
+
42
+ Args:
43
+ path: Bundle path (directory or ZIP)
44
+
45
+ Returns:
46
+ Tuple of (node, encoding, theme, stats, data_info)
47
+ """
48
+ from ._dataclasses import DataInfo, Node
49
+ from .._fig import Encoding, Theme
50
+ from .._stats import Stats
51
+
52
+ storage = get_storage(path)
53
+
54
+ node = None
55
+ encoding = None
56
+ theme = None
57
+ stats = None
58
+ data_info = None
59
+
60
+ # Detect structure: new (canonical/) or legacy (flat)
61
+ # - New: canonical/spec.json
62
+ # - Legacy FTS: node.json at root
63
+ # - Legacy sio.save(): spec.json at root
64
+ if storage.exists("canonical/spec.json"):
65
+ structure = "v2" # New canonical/ structure
66
+ elif storage.exists("spec.json"):
67
+ structure = "sio" # sio.save() structure
68
+ else:
69
+ structure = "v1" # Legacy node.json structure
70
+ is_new_structure = structure == "v2"
71
+
72
+ # Node / spec.json
73
+ if structure == "v2":
74
+ node_data = storage.read_json("canonical/spec.json")
75
+ elif structure == "sio":
76
+ node_data = storage.read_json("spec.json")
77
+ else:
78
+ node_data = storage.read_json("node.json")
79
+ if node_data:
80
+ node = Node.from_dict(node_data)
81
+
82
+ # Encoding
83
+ if is_new_structure:
84
+ encoding_data = storage.read_json("canonical/encoding.json")
85
+ else:
86
+ encoding_data = storage.read_json("encoding.json")
87
+ if encoding_data:
88
+ encoding = Encoding.from_dict(encoding_data)
89
+
90
+ # Theme
91
+ if is_new_structure:
92
+ theme_data = storage.read_json("canonical/theme.json")
93
+ else:
94
+ theme_data = storage.read_json("theme.json")
95
+ if theme_data:
96
+ theme = Theme.from_dict(theme_data)
97
+
98
+ # Stats (payload for kind=stats, or legacy stats/)
99
+ if is_new_structure:
100
+ stats_data = storage.read_json("payload/stats.json")
101
+ else:
102
+ stats_data = storage.read_json("stats/stats.json")
103
+ if stats_data:
104
+ stats = Stats.from_dict(stats_data)
105
+
106
+ # Data info
107
+ if is_new_structure:
108
+ data_info_data = storage.read_json("canonical/data_info.json")
109
+ else:
110
+ data_info_data = storage.read_json("data/data_info.json")
111
+ if data_info_data:
112
+ data_info = DataInfo.from_dict(data_info_data)
113
+
114
+ return node, encoding, theme, stats, data_info
115
+
116
+
117
+ def get_bundle_structure_version(path: Path) -> str:
118
+ """Detect bundle structure version.
119
+
120
+ Args:
121
+ path: Bundle path
122
+
123
+ Returns:
124
+ "v2" for new canonical/ structure, "v1" for legacy flat structure
125
+ """
126
+ storage = get_storage(path)
127
+ if storage.exists("canonical/spec.json"):
128
+ return "v2"
129
+ return "v1"
130
+
131
+
132
+ __all__ = ["load_bundle_components", "get_bundle_structure_version"]
133
+
134
+ # EOF
@@ -0,0 +1,389 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: 2025-12-20
3
+ # File: /home/ywatanabe/proj/scitex-code/src/scitex/fts/_bundle/_mpl_helpers.py
4
+
5
+ """Matplotlib helper functions for FTS bundle creation."""
6
+
7
+ import warnings
8
+ from typing import TYPE_CHECKING, Any, Optional
9
+
10
+ if TYPE_CHECKING:
11
+ from matplotlib.figure import Figure as MplFigure
12
+
13
+ from .._fig import Encoding, Theme
14
+
15
+
16
+ def _get_scitex_axes(fig: "MplFigure") -> Optional[Any]:
17
+ """Find scitex.plt wrapped axes with tracking data.
18
+
19
+ Uses the same helper as sio.save to find objects with export_as_csv.
20
+ """
21
+ try:
22
+ from scitex.io._save_modules._figure_utils import get_figure_with_data
23
+
24
+ return get_figure_with_data(fig)
25
+ except ImportError:
26
+ pass
27
+
28
+ # Fallback: check figure axes directly
29
+ axes_list = list(fig.axes) if hasattr(fig.axes, "__iter__") else [fig.axes]
30
+ for ax in axes_list:
31
+ if hasattr(ax, "export_as_csv") and hasattr(ax, "history"):
32
+ return ax
33
+ return None
34
+
35
+
36
+ def _build_encoding_from_csv_columns(csv_df: "Any") -> "Encoding":
37
+ """Build encoding from actual CSV column names.
38
+
39
+ Handles two formats:
40
+ 1. Verbose: ax-row-{row}-col-{col}_trace-id-{id}_variable-{var}
41
+ 2. Simple: column names like 'x', 'y' (user-provided DataFrames)
42
+
43
+ This ensures encoding references match actual data columns.
44
+ """
45
+ from .._fig._dataclasses import ChannelEncoding, Encoding, TraceEncoding
46
+
47
+ if csv_df is None or csv_df.empty:
48
+ return Encoding(traces=[])
49
+
50
+ from scitex.plt.utils._csv_column_naming import parse_csv_column_name
51
+
52
+ # Group columns by trace (for verbose format)
53
+ trace_columns = {} # {(ax_row, ax_col, trace_id): {variable: column_name}}
54
+
55
+ for col in csv_df.columns:
56
+ parsed = parse_csv_column_name(col)
57
+ if parsed["valid"]:
58
+ key = (parsed["ax_row"], parsed["ax_col"], parsed["trace_id"])
59
+ if key not in trace_columns:
60
+ trace_columns[key] = {}
61
+ trace_columns[key][parsed["variable"]] = col
62
+
63
+ # Build traces from verbose column names
64
+ traces = []
65
+ for (ax_row, ax_col, trace_id), variables in trace_columns.items():
66
+ trace = TraceEncoding(
67
+ trace_id=f"ax-row-{ax_row}-col-{ax_col}_trace-id-{trace_id}",
68
+ x=ChannelEncoding(column=variables.get("x")) if "x" in variables else None,
69
+ y=ChannelEncoding(column=variables.get("y")) if "y" in variables else None,
70
+ )
71
+ traces.append(trace)
72
+
73
+ # If no verbose columns found, try simple column names
74
+ if not traces:
75
+ columns = list(csv_df.columns)
76
+ # Check for common x/y patterns
77
+ x_col = None
78
+ y_col = None
79
+ for col in columns:
80
+ col_lower = col.lower()
81
+ if col_lower in ("x", "time", "index"):
82
+ x_col = col
83
+ elif col_lower in ("y", "value", "values"):
84
+ y_col = col
85
+ # If no pattern match, use first two numeric columns
86
+ if x_col is None or y_col is None:
87
+ numeric_cols = csv_df.select_dtypes(include=["number"]).columns.tolist()
88
+ if len(numeric_cols) >= 2:
89
+ if x_col is None:
90
+ x_col = numeric_cols[0]
91
+ if y_col is None:
92
+ y_col = numeric_cols[1] if numeric_cols[1] != x_col else numeric_cols[0]
93
+ elif len(numeric_cols) == 1:
94
+ if x_col is None:
95
+ x_col = numeric_cols[0]
96
+ if y_col is None:
97
+ y_col = numeric_cols[0]
98
+
99
+ if x_col and y_col:
100
+ trace = TraceEncoding(
101
+ trace_id="main",
102
+ x=ChannelEncoding(column=x_col),
103
+ y=ChannelEncoding(column=y_col),
104
+ )
105
+ traces.append(trace)
106
+
107
+ return Encoding(traces=traces)
108
+
109
+
110
+ def validate_encoding_csv_link(encoding: "Encoding", csv_df: "Any") -> list:
111
+ """Validate that encoding column references exist in CSV data.
112
+
113
+ Args:
114
+ encoding: Encoding object with trace definitions
115
+ csv_df: DataFrame with CSV data
116
+
117
+ Returns:
118
+ List of validation errors (empty if valid)
119
+ """
120
+ errors = []
121
+
122
+ if csv_df is None or csv_df.empty:
123
+ return errors
124
+
125
+ csv_columns = set(csv_df.columns)
126
+
127
+ for trace in encoding.traces:
128
+ if trace.x and trace.x.column:
129
+ if trace.x.column not in csv_columns:
130
+ errors.append(
131
+ f"Encoding references missing column: {trace.x.column}"
132
+ )
133
+ if trace.y and trace.y.column:
134
+ if trace.y.column not in csv_columns:
135
+ errors.append(
136
+ f"Encoding references missing column: {trace.y.column}"
137
+ )
138
+
139
+ return errors
140
+
141
+
142
+ def extract_data_from_mpl_figure(fig: "MplFigure") -> Optional[Any]:
143
+ """Extract plotted data from matplotlib figure.
144
+
145
+ Uses scitex.plt tracking if available (supports all 60+ plot types),
146
+ otherwise falls back to extracting from rendered figure.
147
+ """
148
+ import numpy as np
149
+ import pandas as pd
150
+
151
+ # Try scitex.plt tracking first (supports all plot types)
152
+ scitex_ax = _get_scitex_axes(fig)
153
+ if scitex_ax is not None:
154
+ try:
155
+ csv_df = scitex_ax.export_as_csv()
156
+ if csv_df is not None and not csv_df.empty:
157
+ return csv_df
158
+ except Exception:
159
+ pass
160
+
161
+ # Fallback: extract from rendered figure (limited plot types)
162
+ from ._extractors import extract_bar_data, extract_line_data, extract_scatter_data
163
+
164
+ extracted_data = {}
165
+ axes_list = list(fig.axes) if hasattr(fig.axes, "__iter__") else [fig.axes]
166
+
167
+ for ax_idx, ax in enumerate(axes_list):
168
+ extracted_data.update(extract_line_data(ax, ax_idx))
169
+ extracted_data.update(extract_scatter_data(ax, ax_idx))
170
+ extracted_data.update(extract_bar_data(ax, ax_idx))
171
+
172
+ if not extracted_data:
173
+ return None
174
+
175
+ max_len = max(len(v) for v in extracted_data.values())
176
+ padded = {}
177
+ for k, v in extracted_data.items():
178
+ if len(v) < max_len:
179
+ padded[k] = np.pad(v, (0, max_len - len(v)), constant_values=np.nan)
180
+ else:
181
+ padded[k] = v
182
+
183
+ return pd.DataFrame(padded)
184
+
185
+
186
+ def build_encoding_from_mpl_figure(fig: "MplFigure") -> "Encoding":
187
+ """Build encoding specification from matplotlib figure.
188
+
189
+ Uses scitex.plt tracking if available (captures actual plot method),
190
+ otherwise falls back to detecting from rendered figure.
191
+ """
192
+ from .._fig._dataclasses import Encoding
193
+
194
+ # Try scitex.plt tracking first (knows exact plot method)
195
+ scitex_ax = _get_scitex_axes(fig)
196
+ if scitex_ax is not None and hasattr(scitex_ax, "history") and scitex_ax.history:
197
+ return _build_encoding_from_history(scitex_ax.history)
198
+
199
+ # Fallback: detect from rendered figure
200
+ from ._extractors import build_bar_traces, build_line_traces, build_scatter_traces
201
+
202
+ traces = []
203
+ axes_list = list(fig.axes) if hasattr(fig.axes, "__iter__") else [fig.axes]
204
+
205
+ for ax_idx, ax in enumerate(axes_list):
206
+ traces.extend(build_line_traces(ax, ax_idx))
207
+ traces.extend(build_scatter_traces(ax, ax_idx))
208
+ traces.extend(build_bar_traces(ax, ax_idx))
209
+
210
+ return Encoding(traces=traces)
211
+
212
+
213
+ def build_theme_from_mpl_figure(fig: "MplFigure") -> "Theme":
214
+ """Build theme specification from matplotlib figure."""
215
+ from .._fig import Theme
216
+
217
+ return Theme()
218
+
219
+
220
+ def extract_geometry_from_mpl_figure(fig: "MplFigure") -> dict:
221
+ """Extract geometry data from matplotlib figure for hit testing."""
222
+ try:
223
+ from scitex.plt.utils._hitmap import extract_path_data, extract_selectable_regions
224
+
225
+ return {
226
+ "path_data": extract_path_data(fig),
227
+ "selectable_regions": extract_selectable_regions(fig),
228
+ }
229
+ except Exception:
230
+ return {"elements": []}
231
+
232
+
233
+ def generate_hitmap_from_mpl_figure(fig: "MplFigure", dpi: int = 300) -> tuple:
234
+ """Generate hitmap images from matplotlib figure."""
235
+ try:
236
+ import io
237
+
238
+ from scitex.plt.utils._hitmap import (
239
+ HITMAP_AXES_COLOR,
240
+ HITMAP_BACKGROUND_COLOR,
241
+ apply_hitmap_colors,
242
+ restore_original_colors,
243
+ )
244
+
245
+ axes_list = list(fig.axes) if hasattr(fig.axes, "__iter__") else [fig.axes]
246
+ original_props, color_map, groups = apply_hitmap_colors(fig)
247
+
248
+ saved_fig_facecolor = fig.patch.get_facecolor()
249
+ saved_ax_facecolors = []
250
+ for ax in axes_list:
251
+ saved_ax_facecolors.append(ax.get_facecolor())
252
+ ax.set_facecolor(HITMAP_BACKGROUND_COLOR)
253
+ for spine in ax.spines.values():
254
+ spine.set_color(HITMAP_AXES_COLOR)
255
+ fig.patch.set_facecolor(HITMAP_BACKGROUND_COLOR)
256
+
257
+ png_buf = io.BytesIO()
258
+ with warnings.catch_warnings():
259
+ warnings.filterwarnings("ignore", message=".*tight_layout.*")
260
+ fig.savefig(png_buf, format="png", dpi=dpi, facecolor=HITMAP_BACKGROUND_COLOR)
261
+ png_bytes = png_buf.getvalue()
262
+
263
+ svg_buf = io.BytesIO()
264
+ with warnings.catch_warnings():
265
+ warnings.filterwarnings("ignore", message=".*tight_layout.*")
266
+ fig.savefig(svg_buf, format="svg", facecolor=HITMAP_BACKGROUND_COLOR)
267
+ svg_bytes = svg_buf.getvalue()
268
+
269
+ restore_original_colors(original_props)
270
+ fig.patch.set_facecolor(saved_fig_facecolor)
271
+ for i, ax in enumerate(axes_list):
272
+ ax.set_facecolor(saved_ax_facecolors[i])
273
+
274
+ return png_bytes, svg_bytes
275
+ except Exception:
276
+ return None, None
277
+
278
+
279
+ def from_matplotlib(
280
+ fig: "MplFigure",
281
+ path,
282
+ name: Optional[str] = None,
283
+ csv_df: Optional[Any] = None,
284
+ dpi: int = 300,
285
+ ):
286
+ """Create FTS bundle from matplotlib figure.
287
+
288
+ Args:
289
+ fig: Matplotlib figure object
290
+ path: Output path for bundle (.zip or directory)
291
+ name: Bundle name (defaults to path stem)
292
+ csv_df: Pre-extracted CSV data (uses scitex.plt tracking if None)
293
+ dpi: Resolution for raster exports
294
+
295
+ Note:
296
+ Encoding is built from CSV column names (single source of truth).
297
+ This ensures encoding references always match actual data columns.
298
+ """
299
+ import io
300
+ import json
301
+ from pathlib import Path
302
+
303
+ from ._FTS import FTS
304
+ from ._saver import save_bundle_components
305
+
306
+ path = Path(path)
307
+
308
+ fig_width_inch, fig_height_inch = fig.get_size_inches()
309
+ size_mm = {
310
+ "width": round(fig_width_inch * 25.4, 2),
311
+ "height": round(fig_height_inch * 25.4, 2),
312
+ }
313
+
314
+ bundle = FTS(path, create=True, kind="plot", name=name, size_mm=size_mm)
315
+
316
+ if csv_df is None:
317
+ csv_df = extract_data_from_mpl_figure(fig)
318
+
319
+ if csv_df is not None and not csv_df.empty:
320
+ bundle._node.payload_schema = "scitex.fts.payload.plot@1"
321
+
322
+ # Build encoding from actual CSV columns (single source of truth)
323
+ # This ensures encoding references match real data columns
324
+ if csv_df is not None and not csv_df.empty:
325
+ bundle._encoding = _build_encoding_from_csv_columns(csv_df)
326
+ # Validate encoding-CSV link
327
+ errors = validate_encoding_csv_link(bundle._encoding, csv_df)
328
+ if errors:
329
+ warnings.warn(f"Encoding validation errors: {errors}")
330
+ else:
331
+ bundle._encoding = build_encoding_from_mpl_figure(fig)
332
+ bundle._theme = build_theme_from_mpl_figure(fig)
333
+
334
+ storage = bundle.storage
335
+
336
+ if csv_df is not None and not csv_df.empty:
337
+ csv_bytes = csv_df.to_csv(index=False).encode("utf-8")
338
+ storage.write("payload/data.csv", csv_bytes)
339
+
340
+ data_info = {
341
+ "columns": list(csv_df.columns),
342
+ "dtypes": {col: str(dtype) for col, dtype in csv_df.dtypes.items()},
343
+ "shape": list(csv_df.shape),
344
+ }
345
+ storage.write("canonical/data_info.json", json.dumps(data_info, indent=2).encode())
346
+
347
+ for fmt in ["png", "svg", "pdf"]:
348
+ buf = io.BytesIO()
349
+ with warnings.catch_warnings():
350
+ warnings.filterwarnings("ignore", message=".*tight_layout.*")
351
+ fig.savefig(buf, format=fmt, dpi=dpi, bbox_inches="tight")
352
+ storage.write(f"artifacts/exports/figure.{fmt}", buf.getvalue())
353
+
354
+ geometry = extract_geometry_from_mpl_figure(fig)
355
+ geometry["space"] = "figure_px"
356
+ storage.write("artifacts/cache/geometry_px.json", json.dumps(geometry, indent=2).encode())
357
+
358
+ hitmap_png, hitmap_svg = generate_hitmap_from_mpl_figure(fig, dpi)
359
+ if hitmap_png:
360
+ storage.write("artifacts/cache/hitmap.png", hitmap_png)
361
+ if hitmap_svg:
362
+ storage.write("artifacts/cache/hitmap.svg", hitmap_svg)
363
+
364
+ manifest = {"dpi": dpi, "formats": ["png", "svg", "pdf"], "size_mm": size_mm}
365
+ storage.write("artifacts/cache/render_manifest.json", json.dumps(manifest, indent=2).encode())
366
+
367
+ save_bundle_components(
368
+ path,
369
+ node=bundle._node,
370
+ encoding=bundle._encoding,
371
+ theme=bundle._theme,
372
+ render=False,
373
+ )
374
+
375
+ bundle._dirty = False
376
+ return bundle
377
+
378
+
379
+ __all__ = [
380
+ "extract_data_from_mpl_figure",
381
+ "build_encoding_from_mpl_figure",
382
+ "build_theme_from_mpl_figure",
383
+ "extract_geometry_from_mpl_figure",
384
+ "generate_hitmap_from_mpl_figure",
385
+ "validate_encoding_csv_link",
386
+ "from_matplotlib",
387
+ ]
388
+
389
+ # EOF