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,314 @@
1
+ /**
2
+ * Axis-Based Alignment
3
+ * Scientific plot alignment using axes bounding boxes
4
+ */
5
+
6
+ // ============================================================================
7
+ // Get Axes Bbox for Panel
8
+ // ============================================================================
9
+ function getAxesBboxForPanel(panelName) {
10
+ const cache = panelBboxesCache[panelName];
11
+ if (!cache || !cache.bboxes) return null;
12
+
13
+ const bboxes = cache.bboxes;
14
+
15
+ // Method 1: Look for ax_00_panel, ax_01_panel, etc.
16
+ for (const key of Object.keys(bboxes)) {
17
+ if (key.endsWith('_panel') && key.startsWith('ax_')) {
18
+ const bbox = bboxes[key];
19
+ if (bbox && bbox.x0 !== undefined) {
20
+ return {
21
+ x0: bbox.x0,
22
+ y0: bbox.y0,
23
+ x1: bbox.x1,
24
+ y1: bbox.y1,
25
+ key: key
26
+ };
27
+ }
28
+ }
29
+ }
30
+
31
+ // Method 2: Calculate axes bbox from spine bboxes (xaxis_spine + yaxis_spine)
32
+ // This is the common case for matplotlib figures
33
+ let xSpine = null, ySpine = null;
34
+ for (const key of Object.keys(bboxes)) {
35
+ if (key.endsWith('_xaxis_spine') && key.startsWith('ax_')) {
36
+ xSpine = bboxes[key];
37
+ }
38
+ if (key.endsWith('_yaxis_spine') && key.startsWith('ax_')) {
39
+ ySpine = bboxes[key];
40
+ }
41
+ }
42
+
43
+ if (xSpine && ySpine) {
44
+ // Combine spine bboxes to get axes area
45
+ // Y-spine defines left edge, X-spine defines bottom edge
46
+ const x0 = ySpine.x0 !== undefined ? ySpine.x0 : ySpine.x;
47
+ const y0 = ySpine.y0 !== undefined ? ySpine.y0 : ySpine.y;
48
+ const x1 = xSpine.x1 !== undefined ? xSpine.x1 : (xSpine.x + xSpine.width);
49
+ const y1 = xSpine.y1 !== undefined ? xSpine.y1 : (xSpine.y + xSpine.height);
50
+
51
+ return {
52
+ x0: Math.min(x0, xSpine.x0 || xSpine.x || x0),
53
+ y0: Math.min(y0, xSpine.y0 || xSpine.y || y0),
54
+ x1: Math.max(x1, ySpine.x1 || (ySpine.x + ySpine.width) || x1),
55
+ y1: Math.max(y1, ySpine.y1 || (ySpine.y + ySpine.height) || y1),
56
+ key: 'derived_from_spines'
57
+ };
58
+ }
59
+
60
+ // Method 3: Fallback to _meta.axes_bbox_px for single-axes plots
61
+ if (bboxes._meta && bboxes._meta.axes_bbox_px) {
62
+ const axBbox = bboxes._meta.axes_bbox_px;
63
+ return {
64
+ x0: axBbox.x0 || axBbox.x,
65
+ y0: axBbox.y0 || axBbox.y,
66
+ x1: axBbox.x1 || (axBbox.x + axBbox.width),
67
+ y1: axBbox.y1 || (axBbox.y + axBbox.height),
68
+ key: '_meta.axes_bbox_px'
69
+ };
70
+ }
71
+
72
+ return null;
73
+ }
74
+
75
+ // ============================================================================
76
+ // Calculate Axis Edge Offset
77
+ // ============================================================================
78
+ function getAxisEdgeOffset(panel, axesBbox, edge, imgSize) {
79
+ if (!axesBbox || !imgSize) return 0;
80
+
81
+ // Scale factor from image pixels to displayed panel pixels
82
+ const panelEl = panel;
83
+ const displayWidth = panelEl.offsetWidth;
84
+ const displayHeight = panelEl.offsetHeight;
85
+ const scaleX = displayWidth / imgSize.width;
86
+ const scaleY = displayHeight / imgSize.height;
87
+
88
+ switch(edge) {
89
+ case 'left':
90
+ // Y-axis left edge
91
+ return axesBbox.x0 * scaleX;
92
+ case 'right':
93
+ // Right edge of axes
94
+ return axesBbox.x1 * scaleX;
95
+ case 'top':
96
+ // Top edge of axes
97
+ return axesBbox.y0 * scaleY;
98
+ case 'bottom':
99
+ // X-axis bottom edge
100
+ return axesBbox.y1 * scaleY;
101
+ case 'center-h':
102
+ // Horizontal center of axes
103
+ return ((axesBbox.x0 + axesBbox.x1) / 2) * scaleX;
104
+ case 'center-v':
105
+ // Vertical center of axes
106
+ return ((axesBbox.y0 + axesBbox.y1) / 2) * scaleY;
107
+ default:
108
+ return 0;
109
+ }
110
+ }
111
+
112
+ // ============================================================================
113
+ // Align Panels by Axis
114
+ // ============================================================================
115
+ function alignPanelsByAxis(edge) {
116
+ // Use selected panels only
117
+ const panels = Array.from(document.querySelectorAll('.panel-canvas-item.selected'));
118
+ if (panels.length < 2) {
119
+ setStatus('Select at least 2 panels for axis alignment', true);
120
+ return;
121
+ }
122
+
123
+ // Collect panel info with axes bboxes
124
+ const panelInfos = [];
125
+ for (const panel of panels) {
126
+ const panelName = panel.dataset.panelName;
127
+ const cache = panelBboxesCache[panelName];
128
+ const axesBbox = getAxesBboxForPanel(panelName);
129
+
130
+ if (!axesBbox || !cache) {
131
+ console.warn(`Panel ${panelName}: no axes bbox found`);
132
+ continue;
133
+ }
134
+
135
+ const currentPos = panelPositions[panelName];
136
+ const axisOffset = getAxisEdgeOffset(panel, axesBbox, edge, cache.imgSize);
137
+
138
+ panelInfos.push({
139
+ panel,
140
+ panelName,
141
+ axesBbox,
142
+ imgSize: cache.imgSize,
143
+ currentPos,
144
+ axisOffset
145
+ });
146
+ }
147
+
148
+ if (panelInfos.length < 2) {
149
+ setStatus('Not enough panels with axes info', true);
150
+ return;
151
+ }
152
+
153
+ // Calculate target position - use the first panel's axis position as reference
154
+ const referenceInfo = panelInfos[0];
155
+ const referenceAxisPos = referenceInfo.currentPos.x + (edge.includes('h') || edge === 'left' || edge === 'right' ? referenceInfo.axisOffset : 0);
156
+ const referenceAxisPosY = referenceInfo.currentPos.y + (edge.includes('v') || edge === 'top' || edge === 'bottom' ? referenceInfo.axisOffset : 0);
157
+
158
+ if (edge === 'left' || edge === 'right' || edge === 'center-h') {
159
+ // Align horizontally (match X positions of axis edges)
160
+ // Target = first panel's axis X position in canvas coords
161
+ for (const info of panelInfos) {
162
+ const newX = referenceAxisPos - info.axisOffset;
163
+ info.currentPos.x = newX;
164
+ info.panel.style.left = newX + 'px';
165
+ panelLayoutMm[info.panelName].x_mm = newX / canvasScale;
166
+ }
167
+ } else {
168
+ // Align vertically (match Y positions of axis edges)
169
+ // Target = first panel's axis Y position in canvas coords
170
+ for (const info of panelInfos) {
171
+ const newY = referenceAxisPosY - info.axisOffset;
172
+ info.currentPos.y = newY;
173
+ info.panel.style.top = newY + 'px';
174
+ panelLayoutMm[info.panelName].y_mm = newY / canvasScale;
175
+ }
176
+ }
177
+
178
+ // Update layout data
179
+ updatePanelLayoutFromDOM();
180
+ layoutModified = true;
181
+ }
182
+
183
+ // ============================================================================
184
+ // Stack Panels Vertically
185
+ // ============================================================================
186
+ function stackPanelsVertically() {
187
+ // Use selected panels only
188
+ const panels = Array.from(document.querySelectorAll('.panel-canvas-item.selected'));
189
+ if (panels.length < 2) {
190
+ setStatus('Select at least 2 panels for stacking', true);
191
+ return;
192
+ }
193
+
194
+ // Collect panel info with axes bboxes
195
+ const panelInfos = [];
196
+ for (const panel of panels) {
197
+ const panelName = panel.dataset.panelName;
198
+ const cache = panelBboxesCache[panelName];
199
+ const axesBbox = getAxesBboxForPanel(panelName);
200
+
201
+ if (!axesBbox || !cache) continue;
202
+
203
+ const currentPos = panelPositions[panelName];
204
+ const axisOffsetLeft = getAxisEdgeOffset(panel, axesBbox, 'left', cache.imgSize);
205
+
206
+ panelInfos.push({
207
+ panel,
208
+ panelName,
209
+ axesBbox,
210
+ imgSize: cache.imgSize,
211
+ currentPos,
212
+ axisOffsetLeft,
213
+ height: panel.offsetHeight
214
+ });
215
+ }
216
+
217
+ if (panelInfos.length < 2) return;
218
+
219
+ // Sort by current vertical position
220
+ panelInfos.sort((a, b) => a.currentPos.y - b.currentPos.y);
221
+
222
+ // Use first panel as reference for Y-axis alignment
223
+ const referenceAxisX = panelInfos[0].currentPos.x + panelInfos[0].axisOffsetLeft;
224
+
225
+ // Stack panels vertically with small gap, aligned by Y-axis
226
+ const gap = 10; // pixels
227
+ let currentY = panelInfos[0].currentPos.y;
228
+
229
+ for (const info of panelInfos) {
230
+ // Align Y-axis (left edge of axes)
231
+ const newX = referenceAxisX - info.axisOffsetLeft;
232
+ info.currentPos.x = newX;
233
+ info.panel.style.left = newX + 'px';
234
+
235
+ // Stack vertically
236
+ info.currentPos.y = currentY;
237
+ info.panel.style.top = currentY + 'px';
238
+ currentY += info.height + gap;
239
+ }
240
+
241
+ // Update layout data
242
+ updatePanelLayoutFromDOM();
243
+ layoutModified = true;
244
+ }
245
+
246
+ // ============================================================================
247
+ // Axis Alignment Shortcut Handler
248
+ // ============================================================================
249
+ function handleAlignByAxisShortcut(key) {
250
+ const panels = document.querySelectorAll('.panel-canvas-item');
251
+ if (panels.length < 2) {
252
+ setStatus('Need multiple panels for axis alignment', true);
253
+ return;
254
+ }
255
+
256
+ const dirNames = {
257
+ 'l': 'Y-axis (left edge)',
258
+ 'r': 'Right edge',
259
+ 't': 'Top edge',
260
+ 'b': 'X-axis (bottom edge)',
261
+ 'c': 'Center horizontal',
262
+ 'm': 'Center vertical',
263
+ 's': 'Stacked vertically'
264
+ };
265
+
266
+ switch(key) {
267
+ case 'l': alignPanelsByAxis('left'); break; // Y-axis left
268
+ case 'r': alignPanelsByAxis('right'); break; // Right edge
269
+ case 't': alignPanelsByAxis('top'); break; // Top edge
270
+ case 'b': alignPanelsByAxis('bottom'); break; // X-axis bottom
271
+ case 'c': alignPanelsByAxis('center-h'); break; // Horizontal center
272
+ case 'm': alignPanelsByAxis('center-v'); break; // Vertical center
273
+ case 's': stackPanelsVertically(); break; // Stack with Y-axis alignment
274
+ default:
275
+ setStatus('Unknown axis key: ' + key + '. Use L/R/T/B/C/M/S', true);
276
+ return;
277
+ }
278
+ if (dirNames[key]) {
279
+ setStatus(`Aligned by axis: ${dirNames[key]}`, false);
280
+ }
281
+ }
282
+
283
+ // ============================================================================
284
+ // Panel Movement (Arrow Keys)
285
+ // ============================================================================
286
+ function moveSelectedPanel(direction, amountMm) {
287
+ const selected = document.querySelector('.panel-canvas-item.selected');
288
+ if (!selected) return;
289
+
290
+ const panelName = selected.dataset.panelName;
291
+ const pos = panelPositions[panelName];
292
+ if (!pos) return;
293
+
294
+ switch(direction) {
295
+ case 'left': pos.x -= amountMm * canvasScale; break;
296
+ case 'right': pos.x += amountMm * canvasScale; break;
297
+ case 'up': pos.y -= amountMm * canvasScale; break;
298
+ case 'down': pos.y += amountMm * canvasScale; break;
299
+ }
300
+
301
+ // Update position in pixels (canvasScale = px/mm)
302
+ selected.style.left = pos.x + 'px';
303
+ selected.style.top = pos.y + 'px';
304
+
305
+ // Update layout data
306
+ panelLayoutMm[panelName] = {
307
+ ...panelLayoutMm[panelName],
308
+ x_mm: pos.x / canvasScale,
309
+ y_mm: pos.y / canvasScale
310
+ };
311
+
312
+ layoutModified = true;
313
+ setStatus(`Moved ${panelName} ${direction} by ${amountMm}mm`);
314
+ }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Basic Panel Alignment
3
+ * Bounding box-based alignment (non-scientific)
4
+ */
5
+
6
+ // ============================================================================
7
+ // Basic Alignment (by bounding box)
8
+ // ============================================================================
9
+ function alignPanels(mode) {
10
+ const selectedPanels = getSelectedPanels();
11
+ if (selectedPanels.length < 2) return;
12
+
13
+ // Get bounds
14
+ let targetValue;
15
+ switch(mode) {
16
+ case 'left':
17
+ targetValue = Math.min(...selectedPanels.map(p => p.pos.x));
18
+ selectedPanels.forEach(p => {
19
+ p.pos.x = targetValue;
20
+ p.item.style.left = targetValue + 'px';
21
+ });
22
+ break;
23
+ case 'right':
24
+ targetValue = Math.max(...selectedPanels.map(p => p.pos.x + p.pos.width));
25
+ selectedPanels.forEach(p => {
26
+ p.pos.x = targetValue - p.pos.width;
27
+ p.item.style.left = p.pos.x + 'px';
28
+ });
29
+ break;
30
+ case 'top':
31
+ targetValue = Math.min(...selectedPanels.map(p => p.pos.y));
32
+ selectedPanels.forEach(p => {
33
+ p.pos.y = targetValue;
34
+ p.item.style.top = targetValue + 'px';
35
+ });
36
+ break;
37
+ case 'bottom':
38
+ targetValue = Math.max(...selectedPanels.map(p => p.pos.y + p.pos.height));
39
+ selectedPanels.forEach(p => {
40
+ p.pos.y = targetValue - p.pos.height;
41
+ p.item.style.top = p.pos.y + 'px';
42
+ });
43
+ break;
44
+ case 'center-h':
45
+ const avgX = selectedPanels.reduce((sum, p) => sum + p.pos.x + p.pos.width/2, 0) / selectedPanels.length;
46
+ selectedPanels.forEach(p => {
47
+ p.pos.x = avgX - p.pos.width/2;
48
+ p.item.style.left = p.pos.x + 'px';
49
+ });
50
+ break;
51
+ case 'center-v':
52
+ const avgY = selectedPanels.reduce((sum, p) => sum + p.pos.y + p.pos.height/2, 0) / selectedPanels.length;
53
+ selectedPanels.forEach(p => {
54
+ p.pos.y = avgY - p.pos.height/2;
55
+ p.item.style.top = p.pos.y + 'px';
56
+ });
57
+ break;
58
+ }
59
+
60
+ // Update layout data
61
+ updatePanelLayoutFromDOM();
62
+ layoutModified = true;
63
+ setStatus(`Aligned panels: ${mode}`);
64
+ }
65
+
66
+ // ============================================================================
67
+ // Alignment Shortcut Handler
68
+ // ============================================================================
69
+ function handleAlignShortcut(key, isShift) {
70
+ const panels = document.querySelectorAll('.panel-canvas-item');
71
+ if (panels.length < 2) {
72
+ setStatus('Need multiple panels for alignment', true);
73
+ return;
74
+ }
75
+
76
+ switch(key) {
77
+ case 'l': alignPanels('left'); break;
78
+ case 'r': alignPanels('right'); break;
79
+ case 't': alignPanels('top'); break;
80
+ case 'b': alignPanels('bottom'); break;
81
+ case 'c': alignPanels('center-h'); break;
82
+ case 'm': alignPanels('center-v'); break;
83
+ case 'h': distributePanels('horizontal'); break;
84
+ case 'v': distributePanels('vertical'); break;
85
+ default:
86
+ setStatus('Unknown alignment key: ' + key, true);
87
+ }
88
+ }
89
+
90
+ // ============================================================================
91
+ // Z-Order Management
92
+ // ============================================================================
93
+ function bringPanelToFront() {
94
+ const selected = document.querySelector('.panel-canvas-item.selected');
95
+ if (selected) {
96
+ selected.style.zIndex = (parseInt(selected.style.zIndex || 0) + 1).toString();
97
+ setStatus('Brought panel to front');
98
+ }
99
+ }
100
+
101
+ function sendPanelToBack() {
102
+ const selected = document.querySelector('.panel-canvas-item.selected');
103
+ if (selected) {
104
+ selected.style.zIndex = (parseInt(selected.style.zIndex || 0) - 1).toString();
105
+ setStatus('Sent panel to back');
106
+ }
107
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Panel Distribution
3
+ * Evenly distribute panels horizontally or vertically
4
+ */
5
+
6
+ // ============================================================================
7
+ // Distribute Panels
8
+ // ============================================================================
9
+ function distributePanels(direction) {
10
+ const selectedPanels = getSelectedPanels();
11
+ if (selectedPanels.length < 3) {
12
+ setStatus('Need at least 3 panels for distribution', true);
13
+ return;
14
+ }
15
+
16
+ if (direction === 'horizontal') {
17
+ // Sort by X position
18
+ selectedPanels.sort((a, b) => a.pos.x - b.pos.x);
19
+
20
+ const first = selectedPanels[0];
21
+ const last = selectedPanels[selectedPanels.length - 1];
22
+ const totalSpace = (last.pos.x + last.pos.width) - first.pos.x;
23
+ const totalPanelWidth = selectedPanels.reduce((sum, p) => sum + p.pos.width, 0);
24
+ const gap = (totalSpace - totalPanelWidth) / (selectedPanels.length - 1);
25
+
26
+ let currentX = first.pos.x;
27
+ selectedPanels.forEach(p => {
28
+ p.pos.x = currentX;
29
+ p.item.style.left = currentX + 'px';
30
+ currentX += p.pos.width + gap;
31
+ });
32
+ } else {
33
+ // Sort by Y position
34
+ selectedPanels.sort((a, b) => a.pos.y - b.pos.y);
35
+
36
+ const first = selectedPanels[0];
37
+ const last = selectedPanels[selectedPanels.length - 1];
38
+ const totalSpace = (last.pos.y + last.pos.height) - first.pos.y;
39
+ const totalPanelHeight = selectedPanels.reduce((sum, p) => sum + p.pos.height, 0);
40
+ const gap = (totalSpace - totalPanelHeight) / (selectedPanels.length - 1);
41
+
42
+ let currentY = first.pos.y;
43
+ selectedPanels.forEach(p => {
44
+ p.pos.y = currentY;
45
+ p.item.style.top = currentY + 'px';
46
+ currentY += p.pos.height + gap;
47
+ });
48
+ }
49
+
50
+ // Update layout data
51
+ updatePanelLayoutFromDOM();
52
+ layoutModified = true;
53
+ setStatus(`Distributed panels ${direction}ly`);
54
+ }
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Canvas View Management
3
+ * Handles the unified canvas view for multi-panel figures
4
+ */
5
+
6
+ // ============================================================================
7
+ // Canvas Mode Control
8
+ // ============================================================================
9
+ function setCanvasMode(mode) {
10
+ canvasMode = mode;
11
+ document.getElementById('canvas-grid').classList.toggle('canvas-mode', mode === 'canvas');
12
+ document.getElementById('canvas-grid').classList.toggle('grid-mode', mode === 'grid');
13
+ }
14
+
15
+ // ============================================================================
16
+ // Canvas Rendering
17
+ // ============================================================================
18
+ function renderCanvasView() {
19
+ const container = document.getElementById('canvas-grid');
20
+ container.innerHTML = '';
21
+
22
+ // Fetch panels if not cached
23
+ if (!panelData || !panelData.panels) {
24
+ return;
25
+ }
26
+
27
+ if (canvasMode === 'canvas') {
28
+ // Calculate canvas size based on number of panels
29
+ const panels = panelData.panels;
30
+
31
+ panels.forEach((panel, idx) => {
32
+ const item = document.createElement('div');
33
+ item.className = 'panel-canvas-item';
34
+ item.dataset.panelName = panel.name;
35
+
36
+ // Initialize position if not set
37
+ if (!panelPositions[panel.name]) {
38
+ panelPositions[panel.name] = {
39
+ x: idx * 150,
40
+ y: idx * 150,
41
+ width: panel.width_px || 400,
42
+ height: panel.height_px || 300
43
+ };
44
+ }
45
+
46
+ const pos = panelPositions[panel.name];
47
+ item.style.left = pos.x + 'px';
48
+ item.style.top = pos.y + 'px';
49
+ item.style.width = pos.width + 'px';
50
+ item.style.height = pos.height + 'px';
51
+
52
+ item.innerHTML = `
53
+ <div class="panel-drag-handle">☰</div>
54
+ <div class="panel-label">${panel.name}</div>
55
+ <img src="data:image/png;base64,${panel.image_base64}" style="width: 100%; height: 100%; object-fit: contain;">
56
+ <canvas class="panel-overlay"></canvas>
57
+ `;
58
+
59
+ container.appendChild(item);
60
+
61
+ // Double-click to edit
62
+ item.addEventListener('dblclick', () => {
63
+ loadPanelForEditing(panel.name);
64
+ });
65
+
66
+ // Drag start
67
+ initPanelDrag(item, panel.name);
68
+ });
69
+
70
+ // Update canvas height to fit all panels
71
+ updateCanvasSize();
72
+ } else {
73
+ // Grid mode - use CSS grid layout (simpler)
74
+ loadPanelGrid();
75
+ }
76
+ }
77
+
78
+ // ============================================================================
79
+ // Interactive Element Detection Helper
80
+ // ============================================================================
81
+ function isInteractiveElement(target) {
82
+ // SVG paths with hover-path class are interactive elements
83
+ if (target.classList && target.classList.contains('hover-path')) {
84
+ return true;
85
+ }
86
+ // Check parent elements for hover-path (click might be on child)
87
+ let parent = target.parentElement;
88
+ while (parent) {
89
+ if (parent.tagName === 'path' || parent.classList.contains('hover-path')) {
90
+ // Path elements in SVG overlay are interactive
91
+ return true;
92
+ }
93
+ parent = parent.parentElement;
94
+ }
95
+ return false;
96
+ }
97
+
98
+ // ============================================================================
99
+ // Canvas Size Management
100
+ // ============================================================================
101
+ function updateCanvasSize() {
102
+ // Find the maximum extent of all panels
103
+ let maxX = 0;
104
+ let maxY = 0;
105
+
106
+ Object.values(panelPositions).forEach(pos => {
107
+ maxX = Math.max(maxX, pos.x + pos.width);
108
+ maxY = Math.max(maxY, pos.y + pos.height);
109
+ });
110
+
111
+ // Add some padding
112
+ const container = document.getElementById('canvas-grid');
113
+ if (container && canvasMode === 'canvas') {
114
+ container.style.minHeight = (maxY + 100) + 'px';
115
+ container.style.minWidth = (maxX + 100) + 'px';
116
+ }
117
+ }
118
+
119
+ // ============================================================================
120
+ // Canvas Zoom Functions
121
+ // ============================================================================
122
+ function zoomCanvas(factor) {
123
+ canvasZoom = Math.max(0.1, Math.min(5.0, canvasZoom * factor));
124
+ const container = document.getElementById('canvas-grid');
125
+ if (container) {
126
+ container.style.transform = `scale(${canvasZoom})`;
127
+ container.style.transformOrigin = 'top left';
128
+ }
129
+ }
130
+
131
+ function fitCanvasToWindow() {
132
+ const container = document.getElementById('canvas-grid');
133
+ if (!container) return;
134
+
135
+ const containerWidth = container.scrollWidth;
136
+ const windowWidth = window.innerWidth - 400; // Account for side panels
137
+ canvasZoom = Math.min(1.0, windowWidth / containerWidth);
138
+ container.style.transform = `scale(${canvasZoom})`;
139
+ container.style.transformOrigin = 'top left';
140
+ }
141
+
142
+ function resizeCanvas(factor) {
143
+ const container = document.getElementById('canvas-grid');
144
+ if (!container) return;
145
+
146
+ // Scale all panel positions and sizes
147
+ Object.keys(panelPositions).forEach(name => {
148
+ const pos = panelPositions[name];
149
+ pos.x *= factor;
150
+ pos.y *= factor;
151
+ pos.width *= factor;
152
+ pos.height *= factor;
153
+ });
154
+
155
+ renderCanvasView();
156
+ }
157
+
158
+ // ============================================================================
159
+ // Panel Layout Update from DOM
160
+ // ============================================================================
161
+ function updatePanelLayoutFromDOM() {
162
+ document.querySelectorAll('.panel-canvas-item').forEach(item => {
163
+ const name = item.dataset.panelName;
164
+ const rect = item.getBoundingClientRect();
165
+ panelPositions[name] = {
166
+ x: parseFloat(item.style.left),
167
+ y: parseFloat(item.style.top),
168
+ width: rect.width,
169
+ height: rect.height
170
+ };
171
+ });
172
+ }