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,853 @@
1
+ #!/usr/bin/env python3
2
+ # File: ./src/scitex/vis/editor/flask_editor/renderer.py
3
+ """Figure rendering for Flask editor - supports single and multi-axis figures."""
4
+
5
+ import base64
6
+ import io
7
+ from typing import Any, Dict, Optional, Tuple
8
+
9
+ import matplotlib
10
+
11
+ matplotlib.use("Agg")
12
+ import matplotlib.pyplot as plt
13
+ import numpy as np
14
+ from matplotlib.ticker import MaxNLocator
15
+ from PIL import Image
16
+
17
+ from scitex.plt.styles import get_default_dpi
18
+
19
+ from ._bbox import extract_bboxes, extract_bboxes_multi
20
+ from ._plotter import plot_from_csv, plot_from_recipe
21
+
22
+ # mm to pt conversion factor
23
+ MM_TO_PT = 2.83465
24
+
25
+
26
+ def render_preview_with_bboxes(
27
+ csv_data,
28
+ overrides: Dict[str, Any],
29
+ axis_fontsize: int = 7,
30
+ metadata: Optional[Dict[str, Any]] = None,
31
+ dark_mode: bool = False,
32
+ ) -> Tuple[str, Dict[str, Any], Dict[str, int]]:
33
+ """Render figure and return base64 PNG along with element bounding boxes.
34
+
35
+ Args:
36
+ csv_data: DataFrame containing CSV data
37
+ overrides: Dictionary with override settings
38
+ axis_fontsize: Default font size for axis labels
39
+ metadata: Optional JSON metadata (new schema with axes dict)
40
+ dark_mode: Whether to render with dark mode colors (light text/spines)
41
+
42
+ Returns:
43
+ tuple: (base64_image_data, bboxes_dict, image_size)
44
+ """
45
+ # Check if this is a multi-axis figure (new schema)
46
+ if metadata and "axes" in metadata and isinstance(metadata.get("axes"), dict):
47
+ return render_multi_axis_preview(csv_data, overrides, metadata, dark_mode)
48
+
49
+ # Fall back to single-axis rendering
50
+ return render_single_axis_preview(csv_data, overrides, axis_fontsize, dark_mode)
51
+
52
+
53
+ def render_single_axis_preview(
54
+ csv_data,
55
+ overrides: Dict[str, Any],
56
+ axis_fontsize: int = 7,
57
+ dark_mode: bool = False,
58
+ ) -> Tuple[str, Dict[str, Any], Dict[str, int]]:
59
+ """Render single-axis figure (legacy mode)."""
60
+ o = overrides
61
+
62
+ # Dimensions
63
+ dpi = o.get("dpi", get_default_dpi())
64
+ fig_size = o.get("fig_size", [3.15, 2.68])
65
+
66
+ # Font sizes
67
+ axis_fontsize = o.get("axis_fontsize", 7)
68
+ tick_fontsize = o.get("tick_fontsize", 7)
69
+ title_fontsize = o.get("title_fontsize", 8)
70
+
71
+ # Line/axis thickness
72
+ linewidth_pt = o.get("linewidth", 0.57)
73
+ axis_width_pt = o.get("axis_width", 0.2) * MM_TO_PT
74
+ tick_length_pt = o.get("tick_length", 0.8) * MM_TO_PT
75
+ tick_width_pt = o.get("tick_width", 0.2) * MM_TO_PT
76
+ tick_direction = o.get("tick_direction", "out")
77
+ x_n_ticks = o.get("x_n_ticks", o.get("n_ticks", 4))
78
+ y_n_ticks = o.get("y_n_ticks", o.get("n_ticks", 4))
79
+ hide_x_ticks = o.get("hide_x_ticks", False)
80
+ hide_y_ticks = o.get("hide_y_ticks", False)
81
+
82
+ transparent = o.get("transparent", True)
83
+
84
+ # Create figure
85
+ fig, ax = plt.subplots(figsize=fig_size, dpi=dpi)
86
+ _apply_background(fig, ax, o, transparent)
87
+
88
+ # Plot from CSV data
89
+ if csv_data is not None:
90
+ plot_from_csv(ax, csv_data, overrides, linewidth=linewidth_pt)
91
+ else:
92
+ ax.text(
93
+ 0.5,
94
+ 0.5,
95
+ "No plot data available\n(CSV not found)",
96
+ ha="center",
97
+ va="center",
98
+ transform=ax.transAxes,
99
+ fontsize=axis_fontsize,
100
+ )
101
+
102
+ # Apply labels
103
+ _apply_labels(ax, o, title_fontsize, axis_fontsize)
104
+
105
+ # Tick styling
106
+ _apply_tick_styling(
107
+ ax,
108
+ tick_fontsize,
109
+ tick_length_pt,
110
+ tick_width_pt,
111
+ tick_direction,
112
+ x_n_ticks,
113
+ y_n_ticks,
114
+ hide_x_ticks,
115
+ hide_y_ticks,
116
+ )
117
+
118
+ # Apply grid, limits, spines
119
+ _apply_style(ax, o, axis_width_pt)
120
+
121
+ # Apply annotations
122
+ _apply_annotations(ax, o, axis_fontsize)
123
+
124
+ # Apply caption (below figure)
125
+ caption_artist = _apply_caption(fig, o)
126
+
127
+ # Apply dark mode styling if requested
128
+ if dark_mode:
129
+ _apply_dark_theme(ax)
130
+ # Also style caption if present
131
+ if caption_artist:
132
+ caption_artist.set_color(DARK_THEME_TEXT_COLOR)
133
+
134
+ fig.tight_layout()
135
+
136
+ # Get element bounding boxes BEFORE saving (need renderer)
137
+ fig.canvas.draw()
138
+ renderer = fig.canvas.get_renderer()
139
+
140
+ # Save to buffer first to get actual image size
141
+ buf = io.BytesIO()
142
+ fig.savefig(
143
+ buf, format="png", dpi=dpi, bbox_inches="tight", transparent=transparent
144
+ )
145
+ buf.seek(0)
146
+
147
+ # Get actual saved image dimensions
148
+ img = Image.open(buf)
149
+ img_width, img_height = img.size
150
+ buf.seek(0)
151
+
152
+ # Get bboxes
153
+ bboxes = extract_bboxes(fig, ax, renderer, img_width, img_height)
154
+
155
+ img_data = base64.b64encode(buf.read()).decode("utf-8")
156
+ plt.close(fig)
157
+
158
+ return img_data, bboxes, {"width": img_width, "height": img_height}
159
+
160
+
161
+ def render_multi_axis_preview(
162
+ csv_data,
163
+ overrides: Dict[str, Any],
164
+ metadata: Dict[str, Any],
165
+ dark_mode: bool = False,
166
+ ) -> Tuple[str, Dict[str, Any], Dict[str, int]]:
167
+ """Render multi-axis figure from new schema (scitex.plt.figure.recipe).
168
+
169
+ Args:
170
+ csv_data: DataFrame containing CSV data
171
+ overrides: Dictionary with override settings
172
+ metadata: JSON metadata with axes dict
173
+ dark_mode: Whether to render with dark mode colors
174
+
175
+ Returns:
176
+ tuple: (base64_image_data, bboxes_dict, image_size)
177
+ """
178
+ o = overrides
179
+ axes_spec = metadata.get("axes", {})
180
+ fig_spec = metadata.get("figure", {})
181
+
182
+ # Get grid dimensions from axes positions
183
+ nrows, ncols = _get_grid_dimensions(axes_spec)
184
+
185
+ # Figure dimensions
186
+ dpi = fig_spec.get("dpi", o.get("dpi", get_default_dpi()))
187
+ size_mm = fig_spec.get("size_mm", [176, 106])
188
+ # Convert mm to inches (1 inch = 25.4 mm)
189
+ fig_size = (size_mm[0] / 25.4, size_mm[1] / 25.4)
190
+
191
+ # Font sizes (from overrides)
192
+ axis_fontsize = o.get("axis_fontsize", 7)
193
+ tick_fontsize = o.get("tick_fontsize", 7)
194
+ title_fontsize = o.get("title_fontsize", 8)
195
+
196
+ # Line/axis thickness
197
+ linewidth_pt = o.get("linewidth", 0.57)
198
+ axis_width_pt = o.get("axis_width", 0.2) * MM_TO_PT
199
+ tick_length_pt = o.get("tick_length", 0.8) * MM_TO_PT
200
+ tick_width_pt = o.get("tick_width", 0.2) * MM_TO_PT
201
+ tick_direction = o.get("tick_direction", "out")
202
+ x_n_ticks = o.get("x_n_ticks", o.get("n_ticks", 4))
203
+ y_n_ticks = o.get("y_n_ticks", o.get("n_ticks", 4))
204
+
205
+ transparent = o.get("transparent", True)
206
+
207
+ # Create multi-axis figure
208
+ fig, axes_array = plt.subplots(nrows, ncols, figsize=fig_size, dpi=dpi)
209
+
210
+ # Handle 1D or 2D array
211
+ if nrows == 1 and ncols == 1:
212
+ axes_array = np.array([[axes_array]])
213
+ elif nrows == 1:
214
+ axes_array = axes_array.reshape(1, -1)
215
+ elif ncols == 1:
216
+ axes_array = axes_array.reshape(-1, 1)
217
+
218
+ # Apply background to figure
219
+ if transparent:
220
+ fig.patch.set_facecolor("none")
221
+ elif o.get("facecolor"):
222
+ fig.patch.set_facecolor(o["facecolor"])
223
+
224
+ # Build mapping from axis ID to row/col
225
+ ax_to_rowcol = _build_ax_to_rowcol_map(axes_spec)
226
+
227
+ # Map axes by their ID
228
+ axes_map = {}
229
+ for ax_id, ax_spec in axes_spec.items():
230
+ row, col = ax_to_rowcol.get(ax_id, (0, 0))
231
+ ax = axes_array[row, col]
232
+ axes_map[ax_id] = ax
233
+
234
+ # Apply background
235
+ if transparent:
236
+ ax.patch.set_facecolor("none")
237
+ elif o.get("facecolor"):
238
+ ax.patch.set_facecolor(o["facecolor"])
239
+
240
+ # Plot data - check which schema format
241
+ if csv_data is not None:
242
+ calls = ax_spec.get("calls", [])
243
+ if calls:
244
+ # Recipe schema with explicit calls
245
+ plot_from_recipe(
246
+ ax, csv_data, ax_spec, overrides, linewidth_pt, ax_id=ax_id
247
+ )
248
+ else:
249
+ # Editable schema - plot from CSV column names
250
+ csv_row, csv_col = row, col # Use computed row/col for CSV lookup
251
+ _plot_from_editable_csv(
252
+ ax,
253
+ csv_data,
254
+ ax_id,
255
+ csv_row,
256
+ csv_col,
257
+ overrides,
258
+ linewidth_pt,
259
+ metadata,
260
+ )
261
+
262
+ # Get panel-specific overrides (e.g., ax_00_panel)
263
+ panel_key = f"{ax_id}_panel"
264
+ element_overrides = o.get("element_overrides", {})
265
+ panel_overrides = element_overrides.get(panel_key, {})
266
+
267
+ # Apply axis labels from spec, with panel overrides taking precedence
268
+ xaxis = ax_spec.get("xaxis", {})
269
+ yaxis = ax_spec.get("yaxis", {})
270
+
271
+ # Panel title (from overrides or spec)
272
+ panel_title = panel_overrides.get("title")
273
+ if panel_title:
274
+ ax.set_title(panel_title, fontsize=title_fontsize)
275
+
276
+ # X/Y labels (panel overrides take precedence over spec)
277
+ xlabel = panel_overrides.get("xlabel") or xaxis.get("label")
278
+ ylabel = panel_overrides.get("ylabel") or yaxis.get("label")
279
+
280
+ if xlabel:
281
+ ax.set_xlabel(xlabel, fontsize=axis_fontsize)
282
+ if ylabel:
283
+ ax.set_ylabel(ylabel, fontsize=axis_fontsize)
284
+
285
+ # Apply axis limits
286
+ if xaxis.get("lim"):
287
+ ax.set_xlim(xaxis["lim"])
288
+ if yaxis.get("lim"):
289
+ ax.set_ylim(yaxis["lim"])
290
+
291
+ # Tick styling
292
+ _apply_tick_styling(
293
+ ax,
294
+ tick_fontsize,
295
+ tick_length_pt,
296
+ tick_width_pt,
297
+ tick_direction,
298
+ x_n_ticks,
299
+ y_n_ticks,
300
+ False, # hide_x_ticks
301
+ False, # hide_y_ticks
302
+ )
303
+
304
+ # Apply spines
305
+ if o.get("hide_top_spine", True):
306
+ ax.spines["top"].set_visible(False)
307
+ if o.get("hide_right_spine", True):
308
+ ax.spines["right"].set_visible(False)
309
+ for spine in ax.spines.values():
310
+ spine.set_linewidth(axis_width_pt)
311
+
312
+ # Apply dark mode to this axis
313
+ if dark_mode:
314
+ _apply_dark_theme(ax)
315
+
316
+ # Apply caption (below figure) - use global overrides
317
+ caption_artist = _apply_caption(fig, o)
318
+ if dark_mode and caption_artist:
319
+ caption_artist.set_color(DARK_THEME_TEXT_COLOR)
320
+
321
+ fig.tight_layout()
322
+
323
+ # Get element bounding boxes
324
+ fig.canvas.draw()
325
+ renderer = fig.canvas.get_renderer()
326
+
327
+ # Save to buffer
328
+ buf = io.BytesIO()
329
+ fig.savefig(
330
+ buf, format="png", dpi=dpi, bbox_inches="tight", transparent=transparent
331
+ )
332
+ buf.seek(0)
333
+
334
+ # Get actual saved image dimensions
335
+ img = Image.open(buf)
336
+ img_width, img_height = img.size
337
+ buf.seek(0)
338
+
339
+ # Get bboxes for all axes
340
+ bboxes = extract_bboxes_multi(fig, axes_map, renderer, img_width, img_height)
341
+
342
+ img_data = base64.b64encode(buf.read()).decode("utf-8")
343
+ plt.close(fig)
344
+
345
+ return img_data, bboxes, {"width": img_width, "height": img_height}
346
+
347
+
348
+ def _get_grid_dimensions(axes_spec: Dict[str, Any]) -> Tuple[int, int]:
349
+ """Get grid dimensions from axes specifications.
350
+
351
+ Handles both:
352
+ - New recipe schema with grid_position
353
+ - Editable schema - infer grid from positions (y-position groups = rows)
354
+ """
355
+ # Check if any axis has grid_position
356
+ has_grid_pos = any(ax_spec.get("grid_position") for ax_spec in axes_spec.values())
357
+
358
+ if has_grid_pos:
359
+ max_row = 0
360
+ max_col = 0
361
+ for ax_id, ax_spec in axes_spec.items():
362
+ pos = ax_spec.get("grid_position", {})
363
+ max_row = max(max_row, pos.get("row", 0))
364
+ max_col = max(max_col, pos.get("col", 0))
365
+ return max_row + 1, max_col + 1
366
+
367
+ # Editable schema: infer grid from positions
368
+ # Group by y-position to determine rows
369
+ if not axes_spec:
370
+ return 1, 1
371
+
372
+ positions = []
373
+ for ax_id, ax_spec in axes_spec.items():
374
+ pos = ax_spec.get("position", [0, 0, 0, 0])
375
+ if len(pos) >= 2:
376
+ positions.append((ax_id, pos[0], pos[1])) # (id, x, y)
377
+
378
+ if not positions:
379
+ return 1, 1
380
+
381
+ # Cluster by y-position (tolerance for floating point)
382
+ y_values = sorted(set(round(p[2], 2) for p in positions), reverse=True)
383
+ n_rows = len(y_values)
384
+
385
+ # Count columns in first row
386
+ first_row_y = y_values[0]
387
+ n_cols = sum(1 for p in positions if abs(p[2] - first_row_y) < 0.1)
388
+
389
+ return n_rows, n_cols
390
+
391
+
392
+ def _build_ax_to_rowcol_map(axes_spec: Dict[str, Any]) -> Dict[str, Tuple[int, int]]:
393
+ """Build mapping from axis ID to (row, col) based on positions."""
394
+ if not axes_spec:
395
+ return {}
396
+
397
+ # Check if any axis has grid_position
398
+ has_grid_pos = any(ax_spec.get("grid_position") for ax_spec in axes_spec.values())
399
+
400
+ if has_grid_pos:
401
+ result = {}
402
+ for ax_id, ax_spec in axes_spec.items():
403
+ pos = ax_spec.get("grid_position", {})
404
+ result[ax_id] = (pos.get("row", 0), pos.get("col", 0))
405
+ return result
406
+
407
+ # Editable schema: compute from positions
408
+ positions = []
409
+ for ax_id, ax_spec in axes_spec.items():
410
+ pos = ax_spec.get("position", [0, 0, 0, 0])
411
+ if len(pos) >= 2:
412
+ positions.append((ax_id, pos[0], pos[1])) # (id, x, y)
413
+
414
+ if not positions:
415
+ return dict.fromkeys(axes_spec, (0, 0))
416
+
417
+ # Get unique y-values (rows) - higher y = lower row number (top of figure)
418
+ y_values = sorted(set(round(p[2], 2) for p in positions), reverse=True)
419
+ y_to_row = {y: i for i, y in enumerate(y_values)}
420
+
421
+ # For each row, sort by x to get column
422
+ result = {}
423
+ for row_idx, row_y in enumerate(y_values):
424
+ row_axes = [(ax_id, x) for ax_id, x, y in positions if abs(y - row_y) < 0.1]
425
+ row_axes.sort(key=lambda t: t[1]) # Sort by x
426
+ for col_idx, (ax_id, _) in enumerate(row_axes):
427
+ result[ax_id] = (row_idx, col_idx)
428
+
429
+ return result
430
+
431
+
432
+ def _get_row_col_from_ax_id(
433
+ ax_id: str, ax_map: Optional[Dict[str, Tuple[int, int]]] = None
434
+ ) -> Tuple[int, int]:
435
+ """Extract row and col from axis ID using the mapping."""
436
+ if ax_map and ax_id in ax_map:
437
+ return ax_map[ax_id]
438
+ # Fallback: try parsing ax_XY format
439
+ import re
440
+
441
+ match = re.match(r"ax_(\d)(\d)", ax_id)
442
+ if match:
443
+ return int(match.group(1)), int(match.group(2))
444
+ return 0, 0
445
+
446
+
447
+ def _plot_from_editable_csv(
448
+ ax,
449
+ csv_data,
450
+ ax_id: str,
451
+ row: int,
452
+ col: int,
453
+ overrides: Dict[str, Any],
454
+ linewidth: float,
455
+ metadata: Optional[Dict[str, Any]] = None,
456
+ ):
457
+ """Plot data from editable schema CSV format.
458
+
459
+ CSV columns follow pattern: ax-row-X-col-Y_trace-id-NAME_variable-VAR
460
+ """
461
+ import re
462
+
463
+ df = csv_data
464
+ elements = metadata.get("elements", {}) if metadata else {}
465
+
466
+ # Find columns for this axis (by row/col)
467
+ pattern = f"ax-row-{row}-col-{col}_"
468
+ ax_cols = [c for c in df.columns if c.startswith(pattern)]
469
+
470
+ if not ax_cols:
471
+ return
472
+
473
+ # Group columns by trace-id
474
+ traces = {}
475
+ for col_name in ax_cols:
476
+ # Parse: ax-row-X-col-Y_trace-id-NAME_variable-VAR
477
+ match = re.match(rf"{pattern}trace-id-(.+?)_variable-(.+)", col_name)
478
+ if match:
479
+ trace_id = match.group(1)
480
+ var_name = match.group(2)
481
+ if trace_id not in traces:
482
+ traces[trace_id] = {}
483
+ traces[trace_id][var_name] = col_name
484
+
485
+ # Get element overrides from overrides dict
486
+ element_overrides = overrides.get("element_overrides", {})
487
+
488
+ # Plot each trace
489
+ trace_idx = 0
490
+ for trace_id, vars_dict in traces.items():
491
+ # Find element info from metadata
492
+ element_key = f"{ax_id}_line_{trace_idx:02d}"
493
+ element_info = elements.get(element_key, {})
494
+ label = element_info.get("label", trace_id)
495
+
496
+ # Get user overrides for this trace
497
+ override_key = f"{ax_id}_trace_{trace_idx}"
498
+ trace_overrides = element_overrides.get(override_key, {})
499
+
500
+ # Determine plot type based on variables present
501
+ # Check more specific patterns first, then fall back to x/y
502
+
503
+ if (
504
+ "y_lower" in vars_dict
505
+ and "y_middle" in vars_dict
506
+ and "y_upper" in vars_dict
507
+ ):
508
+ # Shaded line plot (fill_between + line)
509
+ x_col = vars_dict.get("x")
510
+ if x_col:
511
+ x = df[x_col].dropna().values
512
+ y_lower = df[vars_dict["y_lower"]].dropna().values
513
+ y_middle = df[vars_dict["y_middle"]].dropna().values
514
+ y_upper = df[vars_dict["y_upper"]].dropna().values
515
+
516
+ min_len = min(len(x), len(y_lower), len(y_middle), len(y_upper))
517
+ if min_len > 0:
518
+ ax.fill_between(
519
+ x[:min_len], y_lower[:min_len], y_upper[:min_len], alpha=0.3
520
+ )
521
+ ax.plot(x[:min_len], y_middle[:min_len], linewidth=linewidth)
522
+ trace_idx += 1
523
+
524
+ elif "row" in vars_dict and "col" in vars_dict and "value" in vars_dict:
525
+ # Heatmap / imshow
526
+ rows_data = df[vars_dict["row"]].dropna().values
527
+ cols_data = df[vars_dict["col"]].dropna().values
528
+ values = df[vars_dict["value"]].dropna().values
529
+ if len(rows_data) > 0:
530
+ n_rows = int(rows_data.max()) + 1
531
+ n_cols = int(cols_data.max()) + 1
532
+ data = np.zeros((n_rows, n_cols))
533
+ for r, c, v in zip(
534
+ rows_data.astype(int), cols_data.astype(int), values
535
+ ):
536
+ data[r, c] = v
537
+ ax.imshow(data, aspect="auto", origin="lower")
538
+
539
+ elif "y1" in vars_dict and "y2" in vars_dict:
540
+ # fill_between (CI band)
541
+ x_col = vars_dict.get("x")
542
+ if x_col:
543
+ x = df[x_col].dropna().values
544
+ y1 = df[vars_dict["y1"]].dropna().values
545
+ y2 = df[vars_dict["y2"]].dropna().values
546
+ min_len = min(len(x), len(y1), len(y2))
547
+ if min_len > 0:
548
+ ax.fill_between(x[:min_len], y1[:min_len], y2[:min_len], alpha=0.3)
549
+
550
+ elif "yerr" in vars_dict and "y" in vars_dict:
551
+ # Error bars with bar chart
552
+ x_col = vars_dict.get("x")
553
+ y_col = vars_dict.get("y")
554
+ if x_col and y_col:
555
+ x = df[x_col].dropna().values
556
+ y = df[y_col].dropna().values
557
+ yerr = df[vars_dict["yerr"]].dropna().values
558
+ min_len = min(len(x), len(y), len(yerr))
559
+ if min_len > 0:
560
+ ax.bar(x[:min_len], y[:min_len], yerr=yerr[:min_len])
561
+
562
+ elif "group" in vars_dict and "value" in vars_dict:
563
+ # Violin/strip plot - plot as scatter for now
564
+ groups = df[vars_dict["group"]].dropna().values
565
+ values = df[vars_dict["value"]].dropna().values
566
+ if len(groups) > 0 and len(values) > 0:
567
+ min_len = min(len(groups), len(values))
568
+ # Convert string groups to numeric positions
569
+ unique_groups = list(dict.fromkeys(groups[:min_len])) # Preserve order
570
+ group_to_x = {g: i for i, g in enumerate(unique_groups)}
571
+ x_positions = np.array([group_to_x.get(g, 0) for g in groups[:min_len]])
572
+ # Add jitter for strip plot effect
573
+ jitter = np.random.uniform(-0.1, 0.1, min_len)
574
+ ax.scatter(x_positions + jitter, values[:min_len], alpha=0.6, s=20)
575
+ # Set tick labels
576
+ ax.set_xticks(range(len(unique_groups)))
577
+ ax.set_xticklabels(unique_groups, fontsize=6)
578
+
579
+ elif "width" in vars_dict and "height" in vars_dict:
580
+ # Rectangle - skip for now
581
+ pass
582
+
583
+ elif "type" in vars_dict:
584
+ # Skip type-only entries (like stim markers)
585
+ pass
586
+
587
+ elif "content" in vars_dict:
588
+ # Text annotation - skip for preview
589
+ pass
590
+
591
+ elif "x" in vars_dict and "y" in vars_dict:
592
+ # Default: line or scatter plot
593
+ x_col = vars_dict["x"]
594
+ y_col = vars_dict["y"]
595
+ x = df[x_col].dropna().values
596
+ y = df[y_col].dropna().values
597
+ if len(x) > 0 and len(y) > 0:
598
+ min_len = min(len(x), len(y))
599
+ # Apply overrides
600
+ color = trace_overrides.get("color")
601
+ lw = trace_overrides.get("linewidth", linewidth)
602
+ ls = trace_overrides.get("linestyle", "-")
603
+ marker = trace_overrides.get("marker")
604
+ alpha = trace_overrides.get("alpha", 1.0)
605
+
606
+ kwargs = {"linewidth": lw, "linestyle": ls, "alpha": alpha}
607
+ if color:
608
+ kwargs["color"] = color
609
+ if marker:
610
+ kwargs["marker"] = marker
611
+ if label and label != trace_id:
612
+ kwargs["label"] = label
613
+
614
+ # Check if this looks like scatter data (trace-id contains 'scatter' or 'strip')
615
+ if "scatter" in trace_id.lower() or "strip" in trace_id.lower():
616
+ scatter_kwargs = {"alpha": alpha, "s": 20}
617
+ if color:
618
+ scatter_kwargs["c"] = color
619
+ ax.scatter(x[:min_len], y[:min_len], **scatter_kwargs)
620
+ else:
621
+ ax.plot(x[:min_len], y[:min_len], **kwargs)
622
+ trace_idx += 1
623
+
624
+
625
+ def _apply_background(fig, ax, o, transparent):
626
+ """Apply background settings to figure."""
627
+ if transparent:
628
+ fig.patch.set_facecolor("none")
629
+ ax.patch.set_facecolor("none")
630
+ elif o.get("facecolor"):
631
+ fig.patch.set_facecolor(o["facecolor"])
632
+ ax.patch.set_facecolor(o["facecolor"])
633
+
634
+
635
+ def _apply_labels(ax, o, title_fontsize, axis_fontsize):
636
+ """Apply title and axis labels."""
637
+ # Show title only if enabled (default True)
638
+ if o.get("show_title", True) and o.get("title"):
639
+ ax.set_title(o["title"], fontsize=title_fontsize)
640
+ if o.get("xlabel"):
641
+ ax.set_xlabel(o["xlabel"], fontsize=axis_fontsize)
642
+ if o.get("ylabel"):
643
+ ax.set_ylabel(o["ylabel"], fontsize=axis_fontsize)
644
+
645
+
646
+ def _apply_caption(fig, o, caption_fontsize=7):
647
+ """Apply caption below the figure."""
648
+ if not o.get("show_caption", False) or not o.get("caption"):
649
+ return None
650
+
651
+ caption_text = o.get("caption", "")
652
+ fontsize = o.get("caption_fontsize", caption_fontsize)
653
+
654
+ # Place caption below the figure
655
+ # Using fig.text with y position slightly below 0
656
+ caption_artist = fig.text(
657
+ 0.5,
658
+ -0.02, # Centered, below the figure
659
+ caption_text,
660
+ ha="center",
661
+ va="top",
662
+ fontsize=fontsize,
663
+ wrap=True,
664
+ transform=fig.transFigure,
665
+ )
666
+ return caption_artist
667
+
668
+
669
+ def _apply_tick_styling(
670
+ ax,
671
+ tick_fontsize,
672
+ tick_length_pt,
673
+ tick_width_pt,
674
+ tick_direction,
675
+ x_n_ticks,
676
+ y_n_ticks,
677
+ hide_x_ticks,
678
+ hide_y_ticks,
679
+ ):
680
+ """Apply tick styling to axes."""
681
+ ax.tick_params(
682
+ axis="both",
683
+ labelsize=tick_fontsize,
684
+ length=tick_length_pt,
685
+ width=tick_width_pt,
686
+ direction=tick_direction,
687
+ )
688
+
689
+ if hide_x_ticks:
690
+ ax.xaxis.set_ticks([])
691
+ ax.xaxis.set_ticklabels([])
692
+ else:
693
+ ax.xaxis.set_major_locator(MaxNLocator(nbins=x_n_ticks))
694
+ if hide_y_ticks:
695
+ ax.yaxis.set_ticks([])
696
+ ax.yaxis.set_ticklabels([])
697
+ else:
698
+ ax.yaxis.set_major_locator(MaxNLocator(nbins=y_n_ticks))
699
+
700
+
701
+ def _apply_style(ax, o, axis_width_pt):
702
+ """Apply grid, axis limits, and spine settings."""
703
+ if o.get("grid"):
704
+ ax.grid(True, linewidth=axis_width_pt, alpha=0.3)
705
+
706
+ if o.get("xlim"):
707
+ ax.set_xlim(o["xlim"])
708
+ if o.get("ylim"):
709
+ ax.set_ylim(o["ylim"])
710
+
711
+ if o.get("hide_top_spine", True):
712
+ ax.spines["top"].set_visible(False)
713
+ if o.get("hide_right_spine", True):
714
+ ax.spines["right"].set_visible(False)
715
+
716
+ for spine in ax.spines.values():
717
+ spine.set_linewidth(axis_width_pt)
718
+
719
+
720
+ def _apply_annotations(ax, o, axis_fontsize):
721
+ """Apply text annotations to figure."""
722
+ for annot in o.get("annotations", []):
723
+ if annot.get("type") == "text":
724
+ ax.text(
725
+ annot.get("x", 0.5),
726
+ annot.get("y", 0.5),
727
+ annot.get("text", ""),
728
+ transform=ax.transAxes,
729
+ fontsize=annot.get("fontsize", axis_fontsize),
730
+ )
731
+
732
+
733
+ def render_panel_preview(
734
+ panel_dir,
735
+ dark_mode: bool = False,
736
+ ) -> Tuple[Optional[str], Optional[Dict[str, Any]], Optional[Dict[str, int]]]:
737
+ """Render a panel from its pltz bundle directory with dark mode support.
738
+
739
+ Args:
740
+ panel_dir: Path to the .pltz.d panel directory
741
+ dark_mode: Whether to render with dark mode colors
742
+
743
+ Returns:
744
+ tuple: (base64_image_data, bboxes_dict, image_size) or (None, None, None) on error
745
+ """
746
+ import json
747
+ from pathlib import Path
748
+
749
+ import pandas as pd
750
+
751
+ panel_dir = Path(panel_dir)
752
+
753
+ try:
754
+ # Load spec.json
755
+ spec_path = panel_dir / "spec.json"
756
+ if not spec_path.exists():
757
+ # Try legacy format
758
+ for f in panel_dir.glob("*.json"):
759
+ if f.name != "style.json":
760
+ spec_path = f
761
+ break
762
+
763
+ if not spec_path.exists():
764
+ return None, None, None
765
+
766
+ with open(spec_path) as f:
767
+ metadata = json.load(f)
768
+
769
+ # Load CSV data
770
+ csv_data = None
771
+ csv_path = panel_dir / "data.csv"
772
+ if not csv_path.exists():
773
+ for f in panel_dir.glob("*.csv"):
774
+ csv_path = f
775
+ break
776
+
777
+ if csv_path.exists():
778
+ csv_data = pd.read_csv(csv_path)
779
+
780
+ # Load style.json for overrides
781
+ style_path = panel_dir / "style.json"
782
+ overrides = {}
783
+ if style_path.exists():
784
+ with open(style_path) as f:
785
+ style = json.load(f)
786
+ # Convert style to overrides format
787
+ size = style.get("size", {})
788
+ if size:
789
+ width_mm = size.get("width_mm", 80)
790
+ height_mm = size.get("height_mm", 68)
791
+ overrides["fig_size"] = [width_mm / 25.4, height_mm / 25.4]
792
+ overrides["transparent"] = True
793
+
794
+ # Render with dark mode
795
+ return render_preview_with_bboxes(
796
+ csv_data,
797
+ overrides,
798
+ metadata=metadata,
799
+ dark_mode=dark_mode,
800
+ )
801
+
802
+ except Exception as e:
803
+ import traceback
804
+
805
+ print(f"Error rendering panel {panel_dir}: {e}")
806
+ traceback.print_exc()
807
+ return None, None, None
808
+
809
+
810
+ # Dark mode theme colors
811
+ DARK_THEME_TEXT_COLOR = "#e8e8e8" # Light gray for visibility on dark background
812
+ DARK_THEME_SPINE_COLOR = "#e8e8e8"
813
+ DARK_THEME_TICK_COLOR = "#e8e8e8"
814
+
815
+
816
+ def _apply_dark_theme(ax):
817
+ """Apply dark mode colors to axes for visibility on dark backgrounds.
818
+
819
+ Changes title, labels, tick labels, spines, and legend text to light colors.
820
+ """
821
+ # Title
822
+ title = ax.get_title()
823
+ if title:
824
+ ax.title.set_color(DARK_THEME_TEXT_COLOR)
825
+
826
+ # Axis labels
827
+ ax.xaxis.label.set_color(DARK_THEME_TEXT_COLOR)
828
+ ax.yaxis.label.set_color(DARK_THEME_TEXT_COLOR)
829
+
830
+ # Tick labels
831
+ ax.tick_params(
832
+ axis="both", colors=DARK_THEME_TICK_COLOR, labelcolor=DARK_THEME_TEXT_COLOR
833
+ )
834
+
835
+ # Spines
836
+ for spine in ax.spines.values():
837
+ spine.set_color(DARK_THEME_SPINE_COLOR)
838
+
839
+ # Legend (if exists)
840
+ legend = ax.get_legend()
841
+ if legend:
842
+ for text in legend.get_texts():
843
+ text.set_color(DARK_THEME_TEXT_COLOR)
844
+ # Legend title
845
+ legend_title = legend.get_title()
846
+ if legend_title:
847
+ legend_title.set_color(DARK_THEME_TEXT_COLOR)
848
+ # Legend frame (make transparent or dark)
849
+ legend.get_frame().set_facecolor("none")
850
+ legend.get_frame().set_edgecolor(DARK_THEME_SPINE_COLOR)
851
+
852
+
853
+ # EOF