scitex 2.7.0__py3-none-any.whl → 2.7.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (297) hide show
  1. scitex/__init__.py +6 -2
  2. scitex/__version__.py +1 -1
  3. scitex/audio/README.md +52 -0
  4. scitex/audio/__init__.py +384 -0
  5. scitex/audio/__main__.py +129 -0
  6. scitex/audio/_tts.py +334 -0
  7. scitex/audio/engines/__init__.py +44 -0
  8. scitex/audio/engines/base.py +275 -0
  9. scitex/audio/engines/elevenlabs_engine.py +143 -0
  10. scitex/audio/engines/gtts_engine.py +162 -0
  11. scitex/audio/engines/pyttsx3_engine.py +131 -0
  12. scitex/audio/mcp_server.py +757 -0
  13. scitex/bridge/_helpers.py +1 -1
  14. scitex/bridge/_plt_vis.py +1 -1
  15. scitex/bridge/_stats_vis.py +1 -1
  16. scitex/dev/plt/__init__.py +272 -0
  17. scitex/dev/plt/plot_mpl_axhline.py +28 -0
  18. scitex/dev/plt/plot_mpl_axhspan.py +28 -0
  19. scitex/dev/plt/plot_mpl_axvline.py +28 -0
  20. scitex/dev/plt/plot_mpl_axvspan.py +28 -0
  21. scitex/dev/plt/plot_mpl_bar.py +29 -0
  22. scitex/dev/plt/plot_mpl_barh.py +29 -0
  23. scitex/dev/plt/plot_mpl_boxplot.py +28 -0
  24. scitex/dev/plt/plot_mpl_contour.py +31 -0
  25. scitex/dev/plt/plot_mpl_contourf.py +31 -0
  26. scitex/dev/plt/plot_mpl_errorbar.py +30 -0
  27. scitex/dev/plt/plot_mpl_eventplot.py +28 -0
  28. scitex/dev/plt/plot_mpl_fill.py +30 -0
  29. scitex/dev/plt/plot_mpl_fill_between.py +31 -0
  30. scitex/dev/plt/plot_mpl_hexbin.py +28 -0
  31. scitex/dev/plt/plot_mpl_hist.py +28 -0
  32. scitex/dev/plt/plot_mpl_hist2d.py +28 -0
  33. scitex/dev/plt/plot_mpl_imshow.py +29 -0
  34. scitex/dev/plt/plot_mpl_pcolormesh.py +31 -0
  35. scitex/dev/plt/plot_mpl_pie.py +29 -0
  36. scitex/dev/plt/plot_mpl_plot.py +29 -0
  37. scitex/dev/plt/plot_mpl_quiver.py +31 -0
  38. scitex/dev/plt/plot_mpl_scatter.py +28 -0
  39. scitex/dev/plt/plot_mpl_stackplot.py +31 -0
  40. scitex/dev/plt/plot_mpl_stem.py +29 -0
  41. scitex/dev/plt/plot_mpl_step.py +29 -0
  42. scitex/dev/plt/plot_mpl_violinplot.py +28 -0
  43. scitex/dev/plt/plot_sns_barplot.py +29 -0
  44. scitex/dev/plt/plot_sns_boxplot.py +29 -0
  45. scitex/dev/plt/plot_sns_heatmap.py +28 -0
  46. scitex/dev/plt/plot_sns_histplot.py +29 -0
  47. scitex/dev/plt/plot_sns_kdeplot.py +29 -0
  48. scitex/dev/plt/plot_sns_lineplot.py +31 -0
  49. scitex/dev/plt/plot_sns_scatterplot.py +29 -0
  50. scitex/dev/plt/plot_sns_stripplot.py +29 -0
  51. scitex/dev/plt/plot_sns_swarmplot.py +29 -0
  52. scitex/dev/plt/plot_sns_violinplot.py +29 -0
  53. scitex/dev/plt/plot_stx_bar.py +29 -0
  54. scitex/dev/plt/plot_stx_barh.py +29 -0
  55. scitex/dev/plt/plot_stx_box.py +28 -0
  56. scitex/dev/plt/plot_stx_boxplot.py +28 -0
  57. scitex/dev/plt/plot_stx_conf_mat.py +28 -0
  58. scitex/dev/plt/plot_stx_contour.py +31 -0
  59. scitex/dev/plt/plot_stx_ecdf.py +28 -0
  60. scitex/dev/plt/plot_stx_errorbar.py +30 -0
  61. scitex/dev/plt/plot_stx_fill_between.py +31 -0
  62. scitex/dev/plt/plot_stx_fillv.py +28 -0
  63. scitex/dev/plt/plot_stx_heatmap.py +28 -0
  64. scitex/dev/plt/plot_stx_image.py +28 -0
  65. scitex/dev/plt/plot_stx_imshow.py +28 -0
  66. scitex/dev/plt/plot_stx_joyplot.py +28 -0
  67. scitex/dev/plt/plot_stx_kde.py +28 -0
  68. scitex/dev/plt/plot_stx_line.py +28 -0
  69. scitex/dev/plt/plot_stx_mean_ci.py +28 -0
  70. scitex/dev/plt/plot_stx_mean_std.py +28 -0
  71. scitex/dev/plt/plot_stx_median_iqr.py +28 -0
  72. scitex/dev/plt/plot_stx_raster.py +28 -0
  73. scitex/dev/plt/plot_stx_rectangle.py +28 -0
  74. scitex/dev/plt/plot_stx_scatter.py +29 -0
  75. scitex/dev/plt/plot_stx_shaded_line.py +29 -0
  76. scitex/dev/plt/plot_stx_violin.py +28 -0
  77. scitex/dev/plt/plot_stx_violinplot.py +28 -0
  78. scitex/fig/__init__.py +352 -0
  79. scitex/{vis → fig}/backend/_parser.py +1 -1
  80. scitex/{vis → fig}/canvas.py +1 -1
  81. scitex/{vis → fig}/editor/_defaults.py +70 -5
  82. scitex/fig/editor/_edit.py +751 -0
  83. scitex/{vis → fig}/editor/_qt_editor.py +181 -1
  84. scitex/fig/editor/flask_editor/_bbox.py +1276 -0
  85. scitex/fig/editor/flask_editor/_core.py +624 -0
  86. scitex/{vis → fig}/editor/flask_editor/_plotter.py +38 -4
  87. scitex/fig/editor/flask_editor/_renderer.py +739 -0
  88. scitex/{vis → fig}/editor/flask_editor/templates/__init__.py +1 -1
  89. scitex/fig/editor/flask_editor/templates/_html.py +834 -0
  90. scitex/fig/editor/flask_editor/templates/_scripts.py +3136 -0
  91. scitex/{vis → fig}/editor/flask_editor/templates/_styles.py +625 -18
  92. scitex/{vis → fig}/io/__init__.py +13 -1
  93. scitex/fig/io/_bundle.py +973 -0
  94. scitex/{vis → fig}/io/_canvas.py +1 -1
  95. scitex/{vis → fig}/io/_data.py +1 -1
  96. scitex/{vis → fig}/io/_export.py +1 -1
  97. scitex/{vis → fig}/io/_load.py +1 -1
  98. scitex/{vis → fig}/io/_panel.py +1 -1
  99. scitex/{vis → fig}/io/_save.py +1 -1
  100. scitex/{vis → fig}/model/__init__.py +1 -1
  101. scitex/{vis → fig}/model/_annotations.py +1 -1
  102. scitex/{vis → fig}/model/_axes.py +1 -1
  103. scitex/{vis → fig}/model/_figure.py +1 -1
  104. scitex/{vis → fig}/model/_guides.py +1 -1
  105. scitex/{vis → fig}/model/_plot.py +1 -1
  106. scitex/{vis → fig}/model/_styles.py +1 -1
  107. scitex/{vis → fig}/utils/__init__.py +1 -1
  108. scitex/io/__init__.py +10 -26
  109. scitex/io/_bundle.py +434 -0
  110. scitex/io/_flush.py +5 -2
  111. scitex/io/_load.py +98 -0
  112. scitex/io/_load_modules/_H5Explorer.py +5 -2
  113. scitex/io/_load_modules/_canvas.py +2 -2
  114. scitex/io/_load_modules/_image.py +3 -4
  115. scitex/io/_load_modules/_txt.py +4 -2
  116. scitex/io/_metadata.py +34 -324
  117. scitex/io/_metadata_modules/__init__.py +46 -0
  118. scitex/io/_metadata_modules/_embed.py +70 -0
  119. scitex/io/_metadata_modules/_read.py +64 -0
  120. scitex/io/_metadata_modules/_utils.py +79 -0
  121. scitex/io/_metadata_modules/embed_metadata_jpeg.py +74 -0
  122. scitex/io/_metadata_modules/embed_metadata_pdf.py +53 -0
  123. scitex/io/_metadata_modules/embed_metadata_png.py +26 -0
  124. scitex/io/_metadata_modules/embed_metadata_svg.py +62 -0
  125. scitex/io/_metadata_modules/read_metadata_jpeg.py +57 -0
  126. scitex/io/_metadata_modules/read_metadata_pdf.py +51 -0
  127. scitex/io/_metadata_modules/read_metadata_png.py +39 -0
  128. scitex/io/_metadata_modules/read_metadata_svg.py +44 -0
  129. scitex/io/_qr_utils.py +5 -3
  130. scitex/io/_save.py +548 -30
  131. scitex/io/_save_modules/_canvas.py +3 -3
  132. scitex/io/_save_modules/_image.py +5 -9
  133. scitex/io/_save_modules/_tex.py +7 -4
  134. scitex/io/utils/h5_to_zarr.py +11 -9
  135. scitex/msword/__init__.py +255 -0
  136. scitex/msword/profiles.py +357 -0
  137. scitex/msword/reader.py +753 -0
  138. scitex/msword/utils.py +289 -0
  139. scitex/msword/writer.py +362 -0
  140. scitex/plt/__init__.py +5 -2
  141. scitex/plt/_subplots/_AxesWrapper.py +6 -6
  142. scitex/plt/_subplots/_AxisWrapper.py +15 -9
  143. scitex/plt/_subplots/_AxisWrapperMixins/_AdjustmentMixin/__init__.py +36 -0
  144. scitex/plt/_subplots/_AxisWrapperMixins/_AdjustmentMixin/_labels.py +264 -0
  145. scitex/plt/_subplots/_AxisWrapperMixins/_AdjustmentMixin/_metadata.py +213 -0
  146. scitex/plt/_subplots/_AxisWrapperMixins/_AdjustmentMixin/_visual.py +128 -0
  147. scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin/__init__.py +59 -0
  148. scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin/_base.py +34 -0
  149. scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin/_scientific.py +593 -0
  150. scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin/_statistical.py +654 -0
  151. scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin/_stx_aliases.py +527 -0
  152. scitex/plt/_subplots/_AxisWrapperMixins/_RawMatplotlibMixin.py +321 -0
  153. scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin/__init__.py +33 -0
  154. scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin/_base.py +152 -0
  155. scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin/_wrappers.py +600 -0
  156. scitex/plt/_subplots/_AxisWrapperMixins/__init__.py +79 -5
  157. scitex/plt/_subplots/_FigWrapper.py +6 -6
  158. scitex/plt/_subplots/_SubplotsWrapper.py +28 -18
  159. scitex/plt/_subplots/_export_as_csv.py +35 -5
  160. scitex/plt/_subplots/_export_as_csv_formatters/__init__.py +8 -0
  161. scitex/plt/_subplots/_export_as_csv_formatters/_format_annotate.py +10 -21
  162. scitex/plt/_subplots/_export_as_csv_formatters/_format_eventplot.py +18 -7
  163. scitex/plt/_subplots/_export_as_csv_formatters/_format_imshow2d.py +28 -12
  164. scitex/plt/_subplots/_export_as_csv_formatters/_format_matshow.py +10 -4
  165. scitex/plt/_subplots/_export_as_csv_formatters/_format_plot_imshow.py +13 -1
  166. scitex/plt/_subplots/_export_as_csv_formatters/_format_plot_kde.py +12 -2
  167. scitex/plt/_subplots/_export_as_csv_formatters/_format_plot_scatter.py +10 -3
  168. scitex/plt/_subplots/_export_as_csv_formatters/_format_quiver.py +10 -4
  169. scitex/plt/_subplots/_export_as_csv_formatters/_format_sns_jointplot.py +18 -3
  170. scitex/plt/_subplots/_export_as_csv_formatters/_format_sns_lineplot.py +44 -36
  171. scitex/plt/_subplots/_export_as_csv_formatters/_format_sns_pairplot.py +14 -2
  172. scitex/plt/_subplots/_export_as_csv_formatters/_format_streamplot.py +11 -5
  173. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_bar.py +84 -0
  174. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_barh.py +85 -0
  175. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_conf_mat.py +14 -3
  176. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_contour.py +54 -0
  177. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_ecdf.py +14 -2
  178. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_errorbar.py +120 -0
  179. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_heatmap.py +16 -6
  180. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_image.py +29 -19
  181. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_imshow.py +63 -0
  182. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_joyplot.py +22 -5
  183. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_mean_ci.py +18 -14
  184. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_mean_std.py +18 -14
  185. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_median_iqr.py +18 -14
  186. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_raster.py +10 -2
  187. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_scatter.py +51 -0
  188. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_scatter_hist.py +18 -9
  189. scitex/plt/ax/_plot/_stx_ecdf.py +4 -2
  190. scitex/plt/gallery/_generate.py +421 -14
  191. scitex/plt/io/__init__.py +53 -0
  192. scitex/plt/io/_bundle.py +490 -0
  193. scitex/plt/io/_layered_bundle.py +1343 -0
  194. scitex/plt/styles/SCITEX_STYLE.yaml +26 -0
  195. scitex/plt/styles/__init__.py +14 -0
  196. scitex/plt/styles/presets.py +78 -0
  197. scitex/plt/utils/__init__.py +13 -1
  198. scitex/plt/utils/_collect_figure_metadata.py +10 -14
  199. scitex/plt/utils/_configure_mpl.py +6 -18
  200. scitex/plt/utils/_crop.py +32 -14
  201. scitex/plt/utils/_csv_column_naming.py +54 -0
  202. scitex/plt/utils/_figure_mm.py +116 -1
  203. scitex/plt/utils/_hitmap.py +1643 -0
  204. scitex/plt/utils/metadata/__init__.py +25 -0
  205. scitex/plt/utils/metadata/_core.py +9 -10
  206. scitex/plt/utils/metadata/_dimensions.py +6 -3
  207. scitex/plt/utils/metadata/_editable_export.py +405 -0
  208. scitex/plt/utils/metadata/_geometry_extraction.py +570 -0
  209. scitex/schema/__init__.py +109 -16
  210. scitex/schema/_canvas.py +1 -1
  211. scitex/schema/_plot.py +1015 -0
  212. scitex/schema/_stats.py +2 -2
  213. scitex/stats/__init__.py +117 -0
  214. scitex/stats/io/__init__.py +29 -0
  215. scitex/stats/io/_bundle.py +156 -0
  216. scitex/tex/__init__.py +4 -0
  217. scitex/tex/_export.py +890 -0
  218. {scitex-2.7.0.dist-info → scitex-2.7.3.dist-info}/METADATA +11 -1
  219. {scitex-2.7.0.dist-info → scitex-2.7.3.dist-info}/RECORD +238 -170
  220. scitex/io/memo.md +0 -2827
  221. scitex/plt/REQUESTS.md +0 -191
  222. scitex/plt/_subplots/TODO.md +0 -53
  223. scitex/plt/_subplots/_AxisWrapperMixins/_AdjustmentMixin.py +0 -559
  224. scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin.py +0 -1609
  225. scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin.py +0 -447
  226. scitex/plt/templates/research-master/scitex/vis/gallery/area/fill_between.json +0 -110
  227. scitex/plt/templates/research-master/scitex/vis/gallery/area/fill_betweenx.json +0 -88
  228. scitex/plt/templates/research-master/scitex/vis/gallery/area/stx_fill_between.json +0 -103
  229. scitex/plt/templates/research-master/scitex/vis/gallery/area/stx_fillv.json +0 -106
  230. scitex/plt/templates/research-master/scitex/vis/gallery/categorical/bar.json +0 -92
  231. scitex/plt/templates/research-master/scitex/vis/gallery/categorical/barh.json +0 -92
  232. scitex/plt/templates/research-master/scitex/vis/gallery/categorical/boxplot.json +0 -92
  233. scitex/plt/templates/research-master/scitex/vis/gallery/categorical/stx_bar.json +0 -84
  234. scitex/plt/templates/research-master/scitex/vis/gallery/categorical/stx_barh.json +0 -84
  235. scitex/plt/templates/research-master/scitex/vis/gallery/categorical/stx_box.json +0 -83
  236. scitex/plt/templates/research-master/scitex/vis/gallery/categorical/stx_boxplot.json +0 -93
  237. scitex/plt/templates/research-master/scitex/vis/gallery/categorical/stx_violin.json +0 -91
  238. scitex/plt/templates/research-master/scitex/vis/gallery/categorical/stx_violinplot.json +0 -91
  239. scitex/plt/templates/research-master/scitex/vis/gallery/categorical/violinplot.json +0 -91
  240. scitex/plt/templates/research-master/scitex/vis/gallery/contour/contour.json +0 -97
  241. scitex/plt/templates/research-master/scitex/vis/gallery/contour/contourf.json +0 -98
  242. scitex/plt/templates/research-master/scitex/vis/gallery/contour/stx_contour.json +0 -84
  243. scitex/plt/templates/research-master/scitex/vis/gallery/distribution/hist.json +0 -101
  244. scitex/plt/templates/research-master/scitex/vis/gallery/distribution/hist2d.json +0 -96
  245. scitex/plt/templates/research-master/scitex/vis/gallery/distribution/stx_ecdf.json +0 -95
  246. scitex/plt/templates/research-master/scitex/vis/gallery/distribution/stx_joyplot.json +0 -95
  247. scitex/plt/templates/research-master/scitex/vis/gallery/distribution/stx_kde.json +0 -93
  248. scitex/plt/templates/research-master/scitex/vis/gallery/grid/imshow.json +0 -95
  249. scitex/plt/templates/research-master/scitex/vis/gallery/grid/matshow.json +0 -95
  250. scitex/plt/templates/research-master/scitex/vis/gallery/grid/stx_conf_mat.json +0 -83
  251. scitex/plt/templates/research-master/scitex/vis/gallery/grid/stx_heatmap.json +0 -92
  252. scitex/plt/templates/research-master/scitex/vis/gallery/grid/stx_image.json +0 -121
  253. scitex/plt/templates/research-master/scitex/vis/gallery/grid/stx_imshow.json +0 -84
  254. scitex/plt/templates/research-master/scitex/vis/gallery/line/plot.json +0 -110
  255. scitex/plt/templates/research-master/scitex/vis/gallery/line/step.json +0 -92
  256. scitex/plt/templates/research-master/scitex/vis/gallery/line/stx_line.json +0 -95
  257. scitex/plt/templates/research-master/scitex/vis/gallery/line/stx_shaded_line.json +0 -96
  258. scitex/plt/templates/research-master/scitex/vis/gallery/scatter/hexbin.json +0 -95
  259. scitex/plt/templates/research-master/scitex/vis/gallery/scatter/scatter.json +0 -95
  260. scitex/plt/templates/research-master/scitex/vis/gallery/scatter/stem.json +0 -92
  261. scitex/plt/templates/research-master/scitex/vis/gallery/scatter/stx_scatter.json +0 -84
  262. scitex/plt/templates/research-master/scitex/vis/gallery/special/pie.json +0 -94
  263. scitex/plt/templates/research-master/scitex/vis/gallery/special/stx_raster.json +0 -109
  264. scitex/plt/templates/research-master/scitex/vis/gallery/special/stx_rectangle.json +0 -108
  265. scitex/plt/templates/research-master/scitex/vis/gallery/statistical/errorbar.json +0 -93
  266. scitex/plt/templates/research-master/scitex/vis/gallery/statistical/stx_errorbar.json +0 -84
  267. scitex/plt/templates/research-master/scitex/vis/gallery/statistical/stx_mean_ci.json +0 -96
  268. scitex/plt/templates/research-master/scitex/vis/gallery/statistical/stx_mean_std.json +0 -96
  269. scitex/plt/templates/research-master/scitex/vis/gallery/statistical/stx_median_iqr.json +0 -96
  270. scitex/plt/templates/research-master/scitex/vis/gallery/vector/quiver.json +0 -99
  271. scitex/plt/templates/research-master/scitex/vis/gallery/vector/streamplot.json +0 -100
  272. scitex/vis/__init__.py +0 -177
  273. scitex/vis/editor/_edit.py +0 -390
  274. scitex/vis/editor/flask_editor/_bbox.py +0 -529
  275. scitex/vis/editor/flask_editor/_core.py +0 -168
  276. scitex/vis/editor/flask_editor/_renderer.py +0 -393
  277. scitex/vis/editor/flask_editor/templates/_html.py +0 -513
  278. scitex/vis/editor/flask_editor/templates/_scripts.py +0 -1261
  279. /scitex/{vis → fig}/README.md +0 -0
  280. /scitex/{vis → fig}/backend/__init__.py +0 -0
  281. /scitex/{vis → fig}/backend/_export.py +0 -0
  282. /scitex/{vis → fig}/backend/_render.py +0 -0
  283. /scitex/{vis → fig}/docs/CANVAS_ARCHITECTURE.md +0 -0
  284. /scitex/{vis → fig}/editor/__init__.py +0 -0
  285. /scitex/{vis → fig}/editor/_dearpygui_editor.py +0 -0
  286. /scitex/{vis → fig}/editor/_flask_editor.py +0 -0
  287. /scitex/{vis → fig}/editor/_mpl_editor.py +0 -0
  288. /scitex/{vis → fig}/editor/_tkinter_editor.py +0 -0
  289. /scitex/{vis → fig}/editor/flask_editor/__init__.py +0 -0
  290. /scitex/{vis → fig}/editor/flask_editor/_utils.py +0 -0
  291. /scitex/{vis → fig}/io/_directory.py +0 -0
  292. /scitex/{vis → fig}/model/_plot_types.py +0 -0
  293. /scitex/{vis → fig}/utils/_defaults.py +0 -0
  294. /scitex/{vis → fig}/utils/_validate.py +0 -0
  295. {scitex-2.7.0.dist-info → scitex-2.7.3.dist-info}/WHEEL +0 -0
  296. {scitex-2.7.0.dist-info → scitex-2.7.3.dist-info}/entry_points.txt +0 -0
  297. {scitex-2.7.0.dist-info → scitex-2.7.3.dist-info}/licenses/LICENSE +0 -0
scitex/io/_save.py CHANGED
@@ -161,7 +161,7 @@ def save(
161
161
  auto_crop: bool = True,
162
162
  crop_margin_mm: float = 1.0,
163
163
  metadata_extra: dict = None,
164
- json_schema: str = "recipe",
164
+ json_schema: str = "editable",
165
165
  **kwargs,
166
166
  ) -> None:
167
167
  """
@@ -206,9 +206,10 @@ def save(
206
206
  Default is None.
207
207
  json_schema : str, optional
208
208
  Schema type for JSON metadata output. Options:
209
- - "recipe": Minimal schema with method calls + data refs (default, ~95% smaller)
209
+ - "editable": Schema v0.3.0 with element geometry for interactive editing (default)
210
+ - "recipe": Minimal schema with method calls + data refs
210
211
  - "verbose": Full schema with all artist details
211
- Default is "recipe".
212
+ Default is "editable".
212
213
  **kwargs
213
214
  Additional keyword arguments to pass to the underlying save function of the specific format.
214
215
 
@@ -218,9 +219,16 @@ def save(
218
219
 
219
220
  Notes
220
221
  -----
221
- Supported formats include CSV, NPY, PKL, JOBLIB, PNG, HTML, TIFF, MP4, YAML, JSON, HDF5, PTH, MAT, and CBM.
222
+ Supported formats include CSV, NPY, PKL, JOBLIB, PNG, HTML, TIFF, MP4, YAML, JSON, HDF5, PTH, MAT, CBM,
223
+ and SciTeX bundles (.figz, .pltz, .statsz).
222
224
  The function dynamically selects the appropriate saving mechanism based on the file extension.
223
225
 
226
+ Bundle Formats:
227
+ - .figz: Publication figure bundle (panels dict). Default: ZIP archive.
228
+ - .pltz: Plot bundle (matplotlib figure). Default: directory bundle.
229
+ - .statsz: Statistics bundle (comparisons list). Default: directory bundle.
230
+ - Use .d suffix (e.g., "Figure1.figz.d") to force directory format for .figz.
231
+
224
232
  Examples
225
233
  --------
226
234
  >>> import scitex
@@ -495,6 +503,482 @@ def _symlink_to(spath_final, symlink_to, verbose):
495
503
  logger.success(f"Symlinked: {spath_final} -> {symlink_to_full}")
496
504
 
497
505
 
506
+ def _save_pltz_bundle(obj, spath, as_zip=False, data=None, layered=True, **kwargs):
507
+ """Save a matplotlib figure as a .pltz bundle.
508
+
509
+ Bundle structure v2.0 (layered - default):
510
+ plot.pltz.d/
511
+ spec.json # Semantic: WHAT to plot (canonical)
512
+ style.json # Appearance: HOW it looks (canonical)
513
+ data.csv # Raw data (immutable)
514
+ exports/ # PNG, SVG, hitmap
515
+ cache/ # geometry_px.json, render_manifest.json
516
+
517
+ Bundle structure v1.0 (legacy):
518
+ plot.json - specification (axes, styles, theme, etc.)
519
+ plot.csv - raw data (immutable)
520
+ plot.png - raster export (required)
521
+ plot.svg - vector export (optional)
522
+ plot.pdf - publication export (optional)
523
+
524
+ Parameters
525
+ ----------
526
+ obj : matplotlib.figure.Figure
527
+ The figure to save.
528
+ spath : str or Path
529
+ Output path (e.g., "plot.pltz.d" or "plot.pltz").
530
+ as_zip : bool
531
+ If True, save as ZIP archive.
532
+ data : pandas.DataFrame, optional
533
+ Data to embed in the bundle as plot.csv.
534
+ layered : bool
535
+ If True (default), use new layered format (spec/style/geometry).
536
+ If False, use legacy single JSON format.
537
+ **kwargs
538
+ Additional arguments passed to savefig.
539
+ """
540
+ from pathlib import Path
541
+ import tempfile
542
+ import json
543
+ import numpy as np
544
+ from ._bundle import save_bundle, BundleType
545
+
546
+ p = Path(spath)
547
+
548
+ # Extract basename from path (e.g., "myplot.pltz" -> "myplot", "myplot.pltz.d" -> "myplot")
549
+ basename = p.stem # e.g., "myplot.pltz" or "myplot"
550
+ if basename.endswith('.pltz'):
551
+ basename = basename[:-5] # Remove .pltz suffix
552
+ elif basename.endswith('.d'):
553
+ # Handle myplot.pltz.d -> myplot.pltz -> myplot
554
+ basename = Path(basename).stem
555
+ if basename.endswith('.pltz'):
556
+ basename = basename[:-5]
557
+
558
+ # Extract figure from various matplotlib object types
559
+ import matplotlib.figure
560
+ fig = obj
561
+ if hasattr(obj, 'figure'):
562
+ fig = obj.figure
563
+ elif hasattr(obj, 'fig'):
564
+ fig = obj.fig
565
+
566
+ if not isinstance(fig, matplotlib.figure.Figure):
567
+ raise TypeError(f"Expected matplotlib Figure, got {type(obj).__name__}")
568
+
569
+ dpi = kwargs.pop('dpi', 300)
570
+
571
+ # === Always use layered format ===
572
+ from scitex.plt.io import save_layered_pltz_bundle
573
+ import shutil
574
+ import tempfile
575
+
576
+ # Determine bundle directory path
577
+ if as_zip:
578
+ # For ZIP: save to temp dir, then compress
579
+ temp_dir = Path(tempfile.mkdtemp())
580
+ bundle_dir = temp_dir / f"{basename}.pltz.d"
581
+ zip_path = p if not str(p).endswith('.d') else Path(str(p)[:-2])
582
+ else:
583
+ # For directory: save directly
584
+ bundle_dir = p if str(p).endswith('.d') else Path(str(p) + '.d')
585
+
586
+ # Get CSV data from figure if not provided
587
+ csv_df = data
588
+ if csv_df is None:
589
+ csv_source = _get_figure_with_data(obj)
590
+ if csv_source is not None and hasattr(csv_source, 'export_as_csv'):
591
+ try:
592
+ csv_df = csv_source.export_as_csv()
593
+ except Exception:
594
+ pass
595
+
596
+ save_layered_pltz_bundle(
597
+ fig=fig,
598
+ bundle_dir=bundle_dir,
599
+ basename=basename,
600
+ dpi=dpi,
601
+ csv_df=csv_df,
602
+ )
603
+
604
+ # Compress to ZIP if requested
605
+ if as_zip:
606
+ from ._bundle import pack_bundle
607
+ pack_bundle(bundle_dir, zip_path)
608
+ shutil.rmtree(temp_dir) # Clean up temp directory
609
+
610
+ return # Done with layered format
611
+
612
+ # === Legacy format below (DEPRECATED - kept for reference) ===
613
+
614
+ # Calculate size info
615
+ fig_width_inch, fig_height_inch = fig.get_size_inches()
616
+ fig_dpi = fig.get_dpi()
617
+
618
+ # Build spec according to contract (using basename for file references)
619
+ spec = {
620
+ 'schema': {'name': 'scitex.plt.plot', 'version': '1.0.0'},
621
+ 'backend': 'mpl',
622
+ 'data': {
623
+ 'source': f'{basename}.csv',
624
+ 'path': f'{basename}.csv',
625
+ 'hash': None, # Will be computed after data extraction
626
+ 'columns': [], # Will be populated after data extraction
627
+ },
628
+ 'size': {
629
+ 'width_inch': round(fig_width_inch, 2),
630
+ 'height_inch': round(fig_height_inch, 2),
631
+ 'width_mm': round(fig_width_inch * 25.4, 2),
632
+ 'height_mm': round(fig_height_inch * 25.4, 2),
633
+ 'width_px': int(fig_width_inch * dpi),
634
+ 'height_px': int(fig_height_inch * dpi),
635
+ 'dpi': dpi,
636
+ 'crop_margin_mm': 1.0,
637
+ },
638
+ 'axes': [],
639
+ 'theme': {
640
+ 'mode': 'light',
641
+ 'colors': {
642
+ 'background': 'transparent',
643
+ 'axes_bg': 'white',
644
+ 'text': 'black',
645
+ 'spine': 'black',
646
+ 'tick': 'black',
647
+ }
648
+ },
649
+ }
650
+
651
+ # Extract data from plot lines if no data provided
652
+ extracted_data = {}
653
+
654
+ # Extract axes metadata
655
+ for i, ax in enumerate(fig.axes):
656
+ # Get axes bounding box in figure coordinates (0-1)
657
+ bbox = ax.get_position()
658
+
659
+ ax_info = {
660
+ 'xlabel': ax.get_xlabel() or None,
661
+ 'ylabel': ax.get_ylabel() or None,
662
+ 'title': ax.get_title() or None,
663
+ 'xlim': [round(v, 2) for v in ax.get_xlim()],
664
+ 'ylim': [round(v, 2) for v in ax.get_ylim()],
665
+ 'plot_type': 'line', # Default, could be detected
666
+ # Bounding box in normalized figure coordinates (0-1)
667
+ 'bbox': {
668
+ 'x0': round(bbox.x0, 4),
669
+ 'y0': round(bbox.y0, 4),
670
+ 'x1': round(bbox.x1, 4),
671
+ 'y1': round(bbox.y1, 4),
672
+ 'width': round(bbox.width, 4),
673
+ 'height': round(bbox.height, 4),
674
+ },
675
+ # Bounding box in mm
676
+ 'bbox_mm': {
677
+ 'x0': round(bbox.x0 * fig_width_inch * 25.4, 2),
678
+ 'y0': round(bbox.y0 * fig_height_inch * 25.4, 2),
679
+ 'x1': round(bbox.x1 * fig_width_inch * 25.4, 2),
680
+ 'y1': round(bbox.y1 * fig_height_inch * 25.4, 2),
681
+ 'width': round(bbox.width * fig_width_inch * 25.4, 2),
682
+ 'height': round(bbox.height * fig_height_inch * 25.4, 2),
683
+ },
684
+ # Bounding box in pixels
685
+ 'bbox_px': {
686
+ 'x0': int(bbox.x0 * fig_width_inch * dpi),
687
+ 'y0': int(bbox.y0 * fig_height_inch * dpi),
688
+ 'x1': int(bbox.x1 * fig_width_inch * dpi),
689
+ 'y1': int(bbox.y1 * fig_height_inch * dpi),
690
+ 'width': int(bbox.width * fig_width_inch * dpi),
691
+ 'height': int(bbox.height * fig_height_inch * dpi),
692
+ },
693
+ }
694
+
695
+ # SciTeX-specific axis dimensions
696
+ if hasattr(ax, '_scitex_axes_width_mm'):
697
+ ax_info['axes_width_mm'] = ax._scitex_axes_width_mm
698
+ else:
699
+ ax_info['axes_width_mm'] = round(bbox.width * fig_width_inch * 25.4, 1)
700
+
701
+ if hasattr(ax, '_scitex_axes_height_mm'):
702
+ ax_info['axes_height_mm'] = ax._scitex_axes_height_mm
703
+ else:
704
+ ax_info['axes_height_mm'] = round(bbox.height * fig_height_inch * 25.4, 1)
705
+
706
+ # Extract line data for CSV and build lines array
707
+ lines_info = []
708
+ for j, line in enumerate(ax.get_lines()):
709
+ label = line.get_label()
710
+ if label is None or label.startswith('_'):
711
+ label = f'series_{j}'
712
+ xdata, ydata = line.get_data()
713
+ if len(xdata) > 0:
714
+ col_x = f'{label}_x' if i == 0 else f'ax{i}_{label}_x'
715
+ col_y = f'{label}_y' if i == 0 else f'ax{i}_{label}_y'
716
+ extracted_data[col_x] = np.array(xdata)
717
+ extracted_data[col_y] = np.array(ydata)
718
+
719
+ # Get line color (convert RGBA to hex)
720
+ color = line.get_color()
721
+ if isinstance(color, (list, tuple)):
722
+ import matplotlib.colors as mcolors
723
+ color = mcolors.to_hex(color)
724
+
725
+ lines_info.append({
726
+ 'label': label,
727
+ 'x_col': col_x,
728
+ 'y_col': col_y,
729
+ 'color': color,
730
+ 'linewidth': line.get_linewidth(),
731
+ })
732
+
733
+ if lines_info:
734
+ ax_info['lines'] = lines_info
735
+
736
+ spec['axes'].append(ax_info)
737
+
738
+ # Handle theme from figure
739
+ if hasattr(fig, '_scitex_theme'):
740
+ theme_mode = fig._scitex_theme
741
+ spec['theme']['mode'] = theme_mode
742
+ # Update colors based on theme mode
743
+ if theme_mode == 'dark':
744
+ spec['theme']['colors'] = {
745
+ 'background': 'transparent',
746
+ 'axes_bg': 'transparent',
747
+ 'text': '#e8e8e8',
748
+ 'spine': '#e8e8e8',
749
+ 'tick': '#e8e8e8',
750
+ }
751
+ # Re-apply theme colors to ensure legends and other elements get the correct colors
752
+ from scitex.plt.utils._figure_mm import _apply_theme_colors
753
+ for ax in fig.axes:
754
+ _apply_theme_colors(ax, theme='dark')
755
+
756
+ # Build bundle data (include basename for file naming)
757
+ bundle_data = {'spec': spec, 'basename': basename}
758
+
759
+ # Use provided data or extracted data for CSV
760
+ # Priority: 1) explicit data param, 2) export_as_csv method, 3) line extraction fallback
761
+ csv_df = None
762
+ if data is not None:
763
+ csv_df = data
764
+ bundle_data['data'] = data
765
+ else:
766
+ # Try to use export_as_csv from SciTeX wrapped objects (handles all plot types)
767
+ csv_source = _get_figure_with_data(obj)
768
+ if csv_source is not None and hasattr(csv_source, 'export_as_csv'):
769
+ try:
770
+ csv_df = csv_source.export_as_csv()
771
+ if csv_df is not None and not csv_df.empty:
772
+ bundle_data['data'] = csv_df
773
+ logger.debug(f"CSV data extracted via export_as_csv: {len(csv_df)} rows, {len(csv_df.columns)} cols")
774
+ except Exception as e:
775
+ logger.debug(f"export_as_csv failed: {e}")
776
+ csv_df = None
777
+
778
+ # Fallback to line extraction if export_as_csv didn't work
779
+ if csv_df is None and extracted_data:
780
+ try:
781
+ import pandas as pd
782
+ # Pad arrays to same length
783
+ max_len = max(len(v) for v in extracted_data.values())
784
+ padded = {}
785
+ for k, v in extracted_data.items():
786
+ if len(v) < max_len:
787
+ padded[k] = np.pad(v, (0, max_len - len(v)), constant_values=np.nan)
788
+ else:
789
+ padded[k] = v
790
+ csv_df = pd.DataFrame(padded)
791
+ bundle_data['data'] = csv_df
792
+ logger.debug(f"CSV data extracted via line fallback: {len(csv_df)} rows")
793
+ except ImportError:
794
+ pass
795
+
796
+ # Compute hash and columns for data section
797
+ if csv_df is not None:
798
+ import hashlib
799
+ # Get CSV string for hash computation
800
+ csv_str = csv_df.to_csv(index=False)
801
+ csv_hash = hashlib.sha256(csv_str.encode()).hexdigest()
802
+ spec['data']['hash'] = f'sha256:{csv_hash[:16]}'
803
+ spec['data']['columns'] = list(csv_df.columns)
804
+
805
+ # Save figure to multiple formats
806
+ import warnings
807
+ from PIL import Image as PILImage
808
+ from scitex.plt.utils._hitmap import (
809
+ apply_hitmap_colors, restore_original_colors, extract_path_data,
810
+ extract_selectable_regions, HITMAP_BACKGROUND_COLOR, HITMAP_AXES_COLOR
811
+ )
812
+
813
+ crop_box = None
814
+ color_map = {}
815
+
816
+ with tempfile.TemporaryDirectory() as tmp_dir:
817
+ tmp_path = Path(tmp_dir)
818
+
819
+ # Suppress tight_layout warnings for SciTeX figures with custom axes
820
+ with warnings.catch_warnings():
821
+ warnings.filterwarnings('ignore', message='.*tight_layout.*')
822
+
823
+ # Always use transparent background for SciTeX figures (both light and dark themes)
824
+ use_transparent = True
825
+
826
+ # Save PNG (raster) - required
827
+ png_path = tmp_path / "plot.png"
828
+ fig.savefig(png_path, dpi=dpi, bbox_inches='tight', format='png', transparent=use_transparent)
829
+
830
+ # Save SVG (vector) - optional
831
+ svg_path = tmp_path / "plot.svg"
832
+ fig.savefig(svg_path, bbox_inches='tight', format='svg')
833
+
834
+ # Save PDF (vector) - optional
835
+ pdf_path = tmp_path / "plot.pdf"
836
+ fig.savefig(pdf_path, bbox_inches='tight', format='pdf')
837
+
838
+ # Now generate hitmap by applying ID colors to data elements ONLY
839
+ # Keep axes/spines/labels with original colors to preserve bbox_inches='tight' bounds
840
+ # Also detects logical groups (histogram, bar_series, etc.)
841
+ original_props, color_map, groups = apply_hitmap_colors(fig)
842
+
843
+ # Store original background colors and set hitmap colors
844
+ original_fig_facecolor = fig.patch.get_facecolor()
845
+ original_ax_facecolors = []
846
+ original_ax_props = []
847
+ for ax in fig.axes:
848
+ original_ax_facecolors.append(ax.get_facecolor())
849
+ # Store axis element colors for restoration
850
+ ax_props = {
851
+ 'ax': ax,
852
+ 'spine_colors': {k: v.get_edgecolor() for k, v in ax.spines.items()},
853
+ 'tick_colors': ax.tick_params, # Will restore later
854
+ 'xlabel_color': ax.xaxis.label.get_color(),
855
+ 'ylabel_color': ax.yaxis.label.get_color(),
856
+ 'title_color': ax.title.get_color(),
857
+ }
858
+ original_ax_props.append(ax_props)
859
+ # Set hitmap colors for non-data elements
860
+ ax.set_facecolor(HITMAP_BACKGROUND_COLOR)
861
+ for spine in ax.spines.values():
862
+ spine.set_color(HITMAP_AXES_COLOR)
863
+ ax.tick_params(colors=HITMAP_AXES_COLOR, labelcolor=HITMAP_AXES_COLOR)
864
+ ax.xaxis.label.set_color(HITMAP_AXES_COLOR)
865
+ ax.yaxis.label.set_color(HITMAP_AXES_COLOR)
866
+ ax.title.set_color(HITMAP_AXES_COLOR)
867
+
868
+ fig.patch.set_facecolor(HITMAP_BACKGROUND_COLOR)
869
+
870
+ # Save hitmap PNG with same bbox_inches='tight'
871
+ hitmap_path = tmp_path / "plot_hitmap.png"
872
+ fig.savefig(hitmap_path, dpi=dpi, bbox_inches='tight', format='png', facecolor=HITMAP_BACKGROUND_COLOR)
873
+
874
+ # Optimize hitmap PNG size using zlib compression
875
+ try:
876
+ hitmap_img = PILImage.open(hitmap_path).convert('RGB')
877
+ hitmap_img.save(hitmap_path, format='PNG', optimize=True, compress_level=9)
878
+ except Exception:
879
+ pass # Keep original if optimization fails
880
+
881
+ # Save hitmap SVG with same bbox_inches='tight'
882
+ hitmap_svg_path = tmp_path / "plot_hitmap.svg"
883
+ fig.savefig(hitmap_svg_path, bbox_inches='tight', format='svg')
884
+
885
+ # Restore original colors (data elements)
886
+ restore_original_colors(original_props)
887
+
888
+ # Restore original figure and axes colors
889
+ fig.patch.set_facecolor(original_fig_facecolor)
890
+ for i, ax in enumerate(fig.axes):
891
+ ax.set_facecolor(original_ax_facecolors[i])
892
+ if i < len(original_ax_props):
893
+ props = original_ax_props[i]
894
+ for spine_name, color in props['spine_colors'].items():
895
+ ax.spines[spine_name].set_edgecolor(color)
896
+ ax.xaxis.label.set_color(props['xlabel_color'])
897
+ ax.yaxis.label.set_color(props['ylabel_color'])
898
+ ax.title.set_color(props['title_color'])
899
+
900
+ # Now apply auto-crop to BOTH PNG and hitmap with same parameters
901
+ try:
902
+ from scitex.plt.utils._crop import crop
903
+
904
+ # Crop PNG and get crop coordinates
905
+ _, crop_offset = crop(
906
+ str(png_path),
907
+ output_path=str(png_path),
908
+ overwrite=True,
909
+ margin=12, # ~1mm at 300 DPI
910
+ verbose=False,
911
+ return_offset=True,
912
+ )
913
+ crop_box = (crop_offset['left'], crop_offset['upper'],
914
+ crop_offset['right'], crop_offset['lower'])
915
+
916
+ # Apply SAME crop to hitmap PNG
917
+ crop(
918
+ str(hitmap_path),
919
+ output_path=str(hitmap_path),
920
+ overwrite=True,
921
+ crop_box=crop_box,
922
+ verbose=False,
923
+ )
924
+ except Exception as e:
925
+ crop_box = None
926
+ logger.debug(f"Crop failed: {e}")
927
+
928
+ # Validate sizes match
929
+ with PILImage.open(png_path) as png_img, PILImage.open(hitmap_path) as hm_img:
930
+ if png_img.size != hm_img.size:
931
+ logger.warning(f"Size mismatch: PNG={png_img.size}, Hitmap={hm_img.size}")
932
+
933
+ with open(png_path, 'rb') as f:
934
+ bundle_data['png'] = f.read()
935
+
936
+ with open(hitmap_path, 'rb') as f:
937
+ bundle_data['hitmap_png'] = f.read()
938
+
939
+ with open(svg_path, 'rb') as f:
940
+ bundle_data['svg'] = f.read()
941
+
942
+ with open(hitmap_svg_path, 'rb') as f:
943
+ bundle_data['hitmap_svg'] = f.read()
944
+
945
+ with open(pdf_path, 'rb') as f:
946
+ bundle_data['pdf'] = f.read()
947
+
948
+ # Add hit_regions to spec
949
+ try:
950
+ path_data = extract_path_data(fig)
951
+
952
+ spec['hit_regions'] = {
953
+ 'strategy': 'hybrid',
954
+ 'hit_map': f'{basename}_hitmap.png',
955
+ 'hit_map_svg': f'{basename}_hitmap.svg',
956
+ 'color_map': {str(k): v for k, v in color_map.items()},
957
+ 'groups': groups, # Logical groups (histogram, bar_series, etc.)
958
+ 'path_data': path_data,
959
+ }
960
+
961
+ if crop_box is not None:
962
+ spec['hit_regions']['crop_box'] = {
963
+ 'left': int(crop_box[0]),
964
+ 'upper': int(crop_box[1]),
965
+ 'right': int(crop_box[2]),
966
+ 'lower': int(crop_box[3]),
967
+ }
968
+
969
+ # Extract selectable regions (bounding boxes for axis/annotation elements)
970
+ # This complements hitmap color-based selection with bbox-based selection
971
+ selectable_regions = extract_selectable_regions(fig)
972
+ if selectable_regions and selectable_regions.get('axes'):
973
+ spec['selectable_regions'] = selectable_regions
974
+
975
+ except Exception as e:
976
+ logger.debug(f"Hit regions spec failed: {e}")
977
+
978
+ # Save the bundle
979
+ save_bundle(bundle_data, p, bundle_type=BundleType.PLTZ, as_zip=as_zip)
980
+
981
+
498
982
  def _save(
499
983
  obj,
500
984
  spath,
@@ -506,7 +990,7 @@ def _save(
506
990
  auto_crop=False,
507
991
  crop_margin_mm=1.0,
508
992
  metadata_extra=None,
509
- json_schema="recipe",
993
+ json_schema="editable",
510
994
  **kwargs,
511
995
  ):
512
996
  # Don't use object's own save method - use consistent handlers
@@ -521,6 +1005,50 @@ def _save(
521
1005
  save_canvas(obj, spath, **kwargs)
522
1006
  return
523
1007
 
1008
+ # Handle bundle formats (.figz, .pltz, .statsz and their .d variants)
1009
+ # These use special naming: file.figz (ZIP) or file.figz.d (directory)
1010
+ # Note: .figz defaults to ZIP (as_zip=True), .pltz/.statsz default to directory
1011
+ bundle_extensions = (".figz", ".pltz", ".statsz")
1012
+ for bext in bundle_extensions:
1013
+ if spath.endswith(bext) or spath.endswith(f"{bext}.d"):
1014
+ # Remove as_zip from kwargs if present to avoid duplicate
1015
+ bundle_kwargs = {k: v for k, v in kwargs.items() if k != 'as_zip'}
1016
+ as_zip = kwargs.get('as_zip', not spath.endswith(".d"))
1017
+ if bext == ".figz":
1018
+ import scitex.fig as sfig
1019
+ # figz defaults to ZIP, so always pass as_zip explicitly
1020
+ sfig.save_figz(obj, spath, as_zip=as_zip, **bundle_kwargs)
1021
+ elif bext == ".pltz":
1022
+ _save_pltz_bundle(obj, spath, as_zip=as_zip, **bundle_kwargs)
1023
+ elif bext == ".statsz":
1024
+ import scitex.stats as sstats
1025
+ sstats.save_statsz(obj, spath, as_zip=as_zip, **bundle_kwargs)
1026
+
1027
+ # Log "Saved to:" for bundle formats (consistent with other formats)
1028
+ # For bundles, determine the actual saved path (zip or directory)
1029
+ bundle_path = spath if as_zip else f"{spath}.d" if not spath.endswith(".d") else spath
1030
+
1031
+ if verbose and _os.path.exists(bundle_path):
1032
+ file_size = getsize(bundle_path)
1033
+ file_size = readable_bytes(file_size)
1034
+ try:
1035
+ rel_path = _os.path.relpath(bundle_path, _os.getcwd())
1036
+ except ValueError:
1037
+ rel_path = bundle_path
1038
+ logger.success(f"Saved to: ./{rel_path} ({file_size})")
1039
+
1040
+ # Handle symlinks for bundle formats (consistent with other formats)
1041
+ if symlink_from_cwd and _os.path.exists(bundle_path):
1042
+ # Create symlink from cwd to bundle path
1043
+ bundle_basename = _os.path.basename(bundle_path)
1044
+ bundle_cwd = _os.path.join(_os.getcwd(), bundle_basename)
1045
+ _symlink(bundle_path, bundle_cwd, symlink_from_cwd, verbose)
1046
+
1047
+ if symlink_to and _os.path.exists(bundle_path):
1048
+ _symlink_to(bundle_path, symlink_to, verbose)
1049
+
1050
+ return
1051
+
524
1052
  # Try dispatch dictionary first for O(1) lookup
525
1053
  if ext in _FILE_HANDLERS:
526
1054
  # Check if handler needs special parameters
@@ -560,7 +1088,7 @@ def _save(
560
1088
  elif spath.endswith(".pkl.gz"):
561
1089
  save_pickle_compressed(obj, spath, **kwargs)
562
1090
  else:
563
- warnings.warn(f"Unsupported file format. {spath} was not saved.")
1091
+ logger.warning(f"Unsupported file format. {spath} was not saved.")
564
1092
 
565
1093
  if verbose:
566
1094
  if _os.path.exists(spath):
@@ -657,7 +1185,7 @@ def _handle_image_with_csv(
657
1185
  auto_crop=True,
658
1186
  crop_margin_mm=1.0,
659
1187
  metadata_extra=None,
660
- json_schema="recipe",
1188
+ json_schema="editable",
661
1189
  **kwargs,
662
1190
  ):
663
1191
  """Handle image file saving with optional CSV export and auto-cropping."""
@@ -701,7 +1229,10 @@ def _handle_image_with_csv(
701
1229
 
702
1230
  # Collect metadata using scitex's metadata collector
703
1231
  try:
704
- if json_schema == "recipe":
1232
+ if json_schema == "editable":
1233
+ from scitex.plt.utils.metadata import export_editable_figure
1234
+ auto_metadata = export_editable_figure(fig_mpl)
1235
+ elif json_schema == "recipe":
705
1236
  from scitex.plt.utils import collect_recipe_metadata
706
1237
  auto_metadata = collect_recipe_metadata(
707
1238
  fig_mpl, ax,
@@ -716,15 +1247,14 @@ def _handle_image_with_csv(
716
1247
  kwargs["metadata"] = auto_metadata
717
1248
  collected_metadata = auto_metadata # Save for JSON export
718
1249
  if verbose:
719
- schema_name = "recipe" if json_schema == "recipe" else "verbose"
1250
+ schema_names = {"editable": "editable v0.3", "recipe": "recipe", "verbose": "verbose"}
1251
+ schema_name = schema_names.get(json_schema, json_schema)
720
1252
  logger.info(f" • Auto-collected metadata ({schema_name} schema)")
721
1253
  except ImportError:
722
1254
  pass # collect_figure_metadata not available
723
1255
  except Exception as e:
724
1256
  if verbose:
725
- import warnings
726
-
727
- warnings.warn(f"Could not auto-collect metadata: {e}")
1257
+ logger.warning(f"Could not auto-collect metadata: {e}")
728
1258
  except Exception:
729
1259
  pass # Silently continue if auto-collection fails
730
1260
  else:
@@ -820,9 +1350,7 @@ def _handle_image_with_csv(
820
1350
  )
821
1351
 
822
1352
  except Exception as e:
823
- import warnings
824
-
825
- warnings.warn(f"Auto-crop failed: {e}. Image saved without cropping.")
1353
+ logger.warning(f"Auto-crop failed: {e}. Image saved without cropping.")
826
1354
 
827
1355
  # Handle separate legend saving
828
1356
  _save_separate_legends(
@@ -1057,12 +1585,7 @@ def _handle_image_with_csv(
1057
1585
  )
1058
1586
  _symlink(csv_sigmaplot_path, csv_cwd, True, True)
1059
1587
  except Exception as e:
1060
- import warnings
1061
-
1062
- warnings.warn(f"CSV export failed: {e}")
1063
- import traceback
1064
-
1065
- traceback.print_exc()
1588
+ logger.warning(f"CSV export failed: {e}")
1066
1589
 
1067
1590
  # Save metadata as JSON if collected
1068
1591
  if collected_metadata is not None and not dry_run:
@@ -1109,8 +1632,8 @@ def _handle_image_with_csv(
1109
1632
  )
1110
1633
 
1111
1634
  # Verify CSV/JSON consistency (data_ref must match columns_actual)
1112
- # Only check for verbose schema - recipe schema uses different data_ref structure
1113
- if csv_path and not dry_run and json_schema != "recipe":
1635
+ # Only check for verbose schema - recipe/editable schemas use different data_ref structure
1636
+ if csv_path and not dry_run and json_schema == "verbose":
1114
1637
  from scitex.plt.utils._collect_figure_metadata import (
1115
1638
  assert_csv_json_consistency,
1116
1639
  )
@@ -1181,17 +1704,12 @@ def _handle_image_with_csv(
1181
1704
  # Re-raise assertion errors - these are validation failures that should stop execution
1182
1705
  raise
1183
1706
  except Exception as e:
1184
- import warnings
1185
-
1186
- warnings.warn(f"JSON metadata export failed: {e}")
1187
- import traceback
1188
-
1189
- traceback.print_exc()
1707
+ logger.warning(f"JSON metadata export failed: {e}")
1190
1708
 
1191
1709
 
1192
1710
  # Dispatch dictionary for O(1) file format lookup
1193
1711
  _FILE_HANDLERS = {
1194
- # Canvas directory format (scitex.vis)
1712
+ # Canvas directory format (scitex.fig)
1195
1713
  ".canvas": save_canvas,
1196
1714
  # Excel formats
1197
1715
  ".xlsx": save_excel,