scitex 2.7.0__py3-none-any.whl → 2.8.1__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 (355) 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/diagram/README.md +197 -0
  79. scitex/diagram/__init__.py +48 -0
  80. scitex/diagram/_compile.py +312 -0
  81. scitex/diagram/_diagram.py +355 -0
  82. scitex/diagram/_presets.py +173 -0
  83. scitex/diagram/_schema.py +182 -0
  84. scitex/diagram/_split.py +278 -0
  85. scitex/fig/__init__.py +352 -0
  86. scitex/{vis → fig}/backend/_parser.py +1 -1
  87. scitex/{vis → fig}/canvas.py +1 -1
  88. scitex/{vis → fig}/editor/__init__.py +5 -2
  89. scitex/{vis → fig}/editor/_dearpygui_editor.py +1 -1
  90. scitex/{vis → fig}/editor/_defaults.py +70 -5
  91. scitex/{vis → fig}/editor/_mpl_editor.py +1 -1
  92. scitex/{vis → fig}/editor/_qt_editor.py +182 -2
  93. scitex/{vis → fig}/editor/_tkinter_editor.py +1 -1
  94. scitex/fig/editor/edit/__init__.py +50 -0
  95. scitex/fig/editor/edit/backend_detector.py +109 -0
  96. scitex/fig/editor/edit/bundle_resolver.py +240 -0
  97. scitex/fig/editor/edit/editor_launcher.py +239 -0
  98. scitex/fig/editor/edit/manual_handler.py +53 -0
  99. scitex/fig/editor/edit/panel_loader.py +232 -0
  100. scitex/fig/editor/edit/path_resolver.py +67 -0
  101. scitex/fig/editor/flask_editor/_bbox.py +1299 -0
  102. scitex/fig/editor/flask_editor/_core.py +1429 -0
  103. scitex/{vis → fig}/editor/flask_editor/_plotter.py +38 -4
  104. scitex/fig/editor/flask_editor/_renderer.py +813 -0
  105. scitex/fig/editor/flask_editor/static/css/base/reset.css +41 -0
  106. scitex/fig/editor/flask_editor/static/css/base/typography.css +16 -0
  107. scitex/fig/editor/flask_editor/static/css/base/variables.css +85 -0
  108. scitex/fig/editor/flask_editor/static/css/components/buttons.css +217 -0
  109. scitex/fig/editor/flask_editor/static/css/components/context-menu.css +93 -0
  110. scitex/fig/editor/flask_editor/static/css/components/dropdown.css +57 -0
  111. scitex/fig/editor/flask_editor/static/css/components/forms.css +112 -0
  112. scitex/fig/editor/flask_editor/static/css/components/modal.css +59 -0
  113. scitex/fig/editor/flask_editor/static/css/components/sections.css +212 -0
  114. scitex/fig/editor/flask_editor/static/css/features/canvas.css +176 -0
  115. scitex/fig/editor/flask_editor/static/css/features/element-inspector.css +190 -0
  116. scitex/fig/editor/flask_editor/static/css/features/loading.css +59 -0
  117. scitex/fig/editor/flask_editor/static/css/features/overlay.css +45 -0
  118. scitex/fig/editor/flask_editor/static/css/features/panel-grid.css +95 -0
  119. scitex/fig/editor/flask_editor/static/css/features/selection.css +101 -0
  120. scitex/fig/editor/flask_editor/static/css/features/statistics.css +138 -0
  121. scitex/fig/editor/flask_editor/static/css/index.css +31 -0
  122. scitex/fig/editor/flask_editor/static/css/layout/container.css +7 -0
  123. scitex/fig/editor/flask_editor/static/css/layout/controls.css +56 -0
  124. scitex/fig/editor/flask_editor/static/css/layout/preview.css +78 -0
  125. scitex/fig/editor/flask_editor/static/js/alignment/axis.js +314 -0
  126. scitex/fig/editor/flask_editor/static/js/alignment/basic.js +107 -0
  127. scitex/fig/editor/flask_editor/static/js/alignment/distribute.js +54 -0
  128. scitex/fig/editor/flask_editor/static/js/canvas/canvas.js +172 -0
  129. scitex/fig/editor/flask_editor/static/js/canvas/dragging.js +258 -0
  130. scitex/fig/editor/flask_editor/static/js/canvas/resize.js +48 -0
  131. scitex/fig/editor/flask_editor/static/js/canvas/selection.js +71 -0
  132. scitex/fig/editor/flask_editor/static/js/core/api.js +288 -0
  133. scitex/fig/editor/flask_editor/static/js/core/state.js +143 -0
  134. scitex/fig/editor/flask_editor/static/js/core/utils.js +245 -0
  135. scitex/fig/editor/flask_editor/static/js/dev/element-inspector.js +992 -0
  136. scitex/fig/editor/flask_editor/static/js/editor/bbox.js +339 -0
  137. scitex/fig/editor/flask_editor/static/js/editor/element-drag.js +286 -0
  138. scitex/fig/editor/flask_editor/static/js/editor/overlay.js +371 -0
  139. scitex/fig/editor/flask_editor/static/js/editor/preview.js +293 -0
  140. scitex/fig/editor/flask_editor/static/js/main.js +426 -0
  141. scitex/fig/editor/flask_editor/static/js/shortcuts/context-menu.js +152 -0
  142. scitex/fig/editor/flask_editor/static/js/shortcuts/keyboard.js +265 -0
  143. scitex/fig/editor/flask_editor/static/js/ui/controls.js +184 -0
  144. scitex/fig/editor/flask_editor/static/js/ui/download.js +57 -0
  145. scitex/fig/editor/flask_editor/static/js/ui/help.js +100 -0
  146. scitex/fig/editor/flask_editor/static/js/ui/theme.js +34 -0
  147. scitex/fig/editor/flask_editor/templates/__init__.py +123 -0
  148. scitex/fig/editor/flask_editor/templates/_html.py +852 -0
  149. scitex/fig/editor/flask_editor/templates/_scripts.py +4933 -0
  150. scitex/fig/editor/flask_editor/templates/_styles.py +1658 -0
  151. scitex/{vis → fig}/io/__init__.py +13 -1
  152. scitex/fig/io/_bundle.py +1058 -0
  153. scitex/{vis → fig}/io/_canvas.py +1 -1
  154. scitex/{vis → fig}/io/_data.py +1 -1
  155. scitex/{vis → fig}/io/_export.py +1 -1
  156. scitex/{vis → fig}/io/_load.py +1 -1
  157. scitex/{vis → fig}/io/_panel.py +1 -1
  158. scitex/{vis → fig}/io/_save.py +1 -1
  159. scitex/{vis → fig}/model/__init__.py +1 -1
  160. scitex/{vis → fig}/model/_annotations.py +1 -1
  161. scitex/{vis → fig}/model/_axes.py +1 -1
  162. scitex/{vis → fig}/model/_figure.py +1 -1
  163. scitex/{vis → fig}/model/_guides.py +1 -1
  164. scitex/{vis → fig}/model/_plot.py +1 -1
  165. scitex/{vis → fig}/model/_styles.py +1 -1
  166. scitex/{vis → fig}/utils/__init__.py +1 -1
  167. scitex/io/__init__.py +22 -26
  168. scitex/io/_bundle.py +493 -0
  169. scitex/io/_flush.py +5 -2
  170. scitex/io/_load.py +98 -0
  171. scitex/io/_load_modules/_H5Explorer.py +5 -2
  172. scitex/io/_load_modules/_canvas.py +2 -2
  173. scitex/io/_load_modules/_image.py +3 -4
  174. scitex/io/_load_modules/_txt.py +4 -2
  175. scitex/io/_metadata.py +34 -324
  176. scitex/io/_metadata_modules/__init__.py +46 -0
  177. scitex/io/_metadata_modules/_embed.py +70 -0
  178. scitex/io/_metadata_modules/_read.py +64 -0
  179. scitex/io/_metadata_modules/_utils.py +79 -0
  180. scitex/io/_metadata_modules/embed_metadata_jpeg.py +74 -0
  181. scitex/io/_metadata_modules/embed_metadata_pdf.py +53 -0
  182. scitex/io/_metadata_modules/embed_metadata_png.py +26 -0
  183. scitex/io/_metadata_modules/embed_metadata_svg.py +62 -0
  184. scitex/io/_metadata_modules/read_metadata_jpeg.py +57 -0
  185. scitex/io/_metadata_modules/read_metadata_pdf.py +51 -0
  186. scitex/io/_metadata_modules/read_metadata_png.py +39 -0
  187. scitex/io/_metadata_modules/read_metadata_svg.py +44 -0
  188. scitex/io/_qr_utils.py +5 -3
  189. scitex/io/_save.py +548 -30
  190. scitex/io/_save_modules/_canvas.py +3 -3
  191. scitex/io/_save_modules/_image.py +5 -9
  192. scitex/io/_save_modules/_tex.py +7 -4
  193. scitex/io/_zip_bundle.py +439 -0
  194. scitex/io/utils/h5_to_zarr.py +11 -9
  195. scitex/msword/__init__.py +255 -0
  196. scitex/msword/profiles.py +357 -0
  197. scitex/msword/reader.py +753 -0
  198. scitex/msword/utils.py +289 -0
  199. scitex/msword/writer.py +362 -0
  200. scitex/plt/__init__.py +5 -2
  201. scitex/plt/_subplots/_AxesWrapper.py +6 -6
  202. scitex/plt/_subplots/_AxisWrapper.py +15 -9
  203. scitex/plt/_subplots/_AxisWrapperMixins/_AdjustmentMixin/__init__.py +36 -0
  204. scitex/plt/_subplots/_AxisWrapperMixins/_AdjustmentMixin/_labels.py +264 -0
  205. scitex/plt/_subplots/_AxisWrapperMixins/_AdjustmentMixin/_metadata.py +213 -0
  206. scitex/plt/_subplots/_AxisWrapperMixins/_AdjustmentMixin/_visual.py +128 -0
  207. scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin/__init__.py +59 -0
  208. scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin/_base.py +34 -0
  209. scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin/_scientific.py +593 -0
  210. scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin/_statistical.py +654 -0
  211. scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin/_stx_aliases.py +527 -0
  212. scitex/plt/_subplots/_AxisWrapperMixins/_RawMatplotlibMixin.py +321 -0
  213. scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin/__init__.py +33 -0
  214. scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin/_base.py +152 -0
  215. scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin/_wrappers.py +600 -0
  216. scitex/plt/_subplots/_AxisWrapperMixins/__init__.py +79 -5
  217. scitex/plt/_subplots/_FigWrapper.py +6 -6
  218. scitex/plt/_subplots/_SubplotsWrapper.py +28 -18
  219. scitex/plt/_subplots/_export_as_csv.py +35 -5
  220. scitex/plt/_subplots/_export_as_csv_formatters/__init__.py +8 -0
  221. scitex/plt/_subplots/_export_as_csv_formatters/_format_annotate.py +10 -21
  222. scitex/plt/_subplots/_export_as_csv_formatters/_format_eventplot.py +18 -7
  223. scitex/plt/_subplots/_export_as_csv_formatters/_format_imshow2d.py +28 -12
  224. scitex/plt/_subplots/_export_as_csv_formatters/_format_matshow.py +10 -4
  225. scitex/plt/_subplots/_export_as_csv_formatters/_format_plot_imshow.py +13 -1
  226. scitex/plt/_subplots/_export_as_csv_formatters/_format_plot_kde.py +12 -2
  227. scitex/plt/_subplots/_export_as_csv_formatters/_format_plot_scatter.py +10 -3
  228. scitex/plt/_subplots/_export_as_csv_formatters/_format_quiver.py +10 -4
  229. scitex/plt/_subplots/_export_as_csv_formatters/_format_sns_jointplot.py +18 -3
  230. scitex/plt/_subplots/_export_as_csv_formatters/_format_sns_lineplot.py +44 -36
  231. scitex/plt/_subplots/_export_as_csv_formatters/_format_sns_pairplot.py +14 -2
  232. scitex/plt/_subplots/_export_as_csv_formatters/_format_streamplot.py +11 -5
  233. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_bar.py +84 -0
  234. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_barh.py +85 -0
  235. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_conf_mat.py +14 -3
  236. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_contour.py +54 -0
  237. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_ecdf.py +14 -2
  238. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_errorbar.py +120 -0
  239. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_heatmap.py +16 -6
  240. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_image.py +29 -19
  241. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_imshow.py +63 -0
  242. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_joyplot.py +22 -5
  243. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_mean_ci.py +18 -14
  244. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_mean_std.py +18 -14
  245. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_median_iqr.py +18 -14
  246. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_raster.py +10 -2
  247. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_scatter.py +51 -0
  248. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_scatter_hist.py +18 -9
  249. scitex/plt/ax/_plot/_stx_ecdf.py +4 -2
  250. scitex/plt/gallery/_generate.py +421 -14
  251. scitex/plt/io/__init__.py +53 -0
  252. scitex/plt/io/_bundle.py +490 -0
  253. scitex/plt/io/_layered_bundle.py +1343 -0
  254. scitex/plt/styles/SCITEX_STYLE.yaml +26 -0
  255. scitex/plt/styles/__init__.py +14 -0
  256. scitex/plt/styles/presets.py +78 -0
  257. scitex/plt/utils/__init__.py +13 -1
  258. scitex/plt/utils/_collect_figure_metadata.py +10 -14
  259. scitex/plt/utils/_configure_mpl.py +6 -18
  260. scitex/plt/utils/_crop.py +32 -14
  261. scitex/plt/utils/_csv_column_naming.py +54 -0
  262. scitex/plt/utils/_figure_mm.py +116 -1
  263. scitex/plt/utils/_hitmap.py +1643 -0
  264. scitex/plt/utils/metadata/__init__.py +25 -0
  265. scitex/plt/utils/metadata/_core.py +9 -10
  266. scitex/plt/utils/metadata/_dimensions.py +6 -3
  267. scitex/plt/utils/metadata/_editable_export.py +405 -0
  268. scitex/plt/utils/metadata/_geometry_extraction.py +570 -0
  269. scitex/schema/__init__.py +109 -16
  270. scitex/schema/_canvas.py +1 -1
  271. scitex/schema/_plot.py +1015 -0
  272. scitex/schema/_stats.py +2 -2
  273. scitex/stats/__init__.py +117 -0
  274. scitex/stats/io/__init__.py +29 -0
  275. scitex/stats/io/_bundle.py +156 -0
  276. scitex/tex/__init__.py +4 -0
  277. scitex/tex/_export.py +890 -0
  278. {scitex-2.7.0.dist-info → scitex-2.8.1.dist-info}/METADATA +11 -1
  279. {scitex-2.7.0.dist-info → scitex-2.8.1.dist-info}/RECORD +294 -170
  280. scitex/io/memo.md +0 -2827
  281. scitex/plt/REQUESTS.md +0 -191
  282. scitex/plt/_subplots/TODO.md +0 -53
  283. scitex/plt/_subplots/_AxisWrapperMixins/_AdjustmentMixin.py +0 -559
  284. scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin.py +0 -1609
  285. scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin.py +0 -447
  286. scitex/plt/templates/research-master/scitex/vis/gallery/area/fill_between.json +0 -110
  287. scitex/plt/templates/research-master/scitex/vis/gallery/area/fill_betweenx.json +0 -88
  288. scitex/plt/templates/research-master/scitex/vis/gallery/area/stx_fill_between.json +0 -103
  289. scitex/plt/templates/research-master/scitex/vis/gallery/area/stx_fillv.json +0 -106
  290. scitex/plt/templates/research-master/scitex/vis/gallery/categorical/bar.json +0 -92
  291. scitex/plt/templates/research-master/scitex/vis/gallery/categorical/barh.json +0 -92
  292. scitex/plt/templates/research-master/scitex/vis/gallery/categorical/boxplot.json +0 -92
  293. scitex/plt/templates/research-master/scitex/vis/gallery/categorical/stx_bar.json +0 -84
  294. scitex/plt/templates/research-master/scitex/vis/gallery/categorical/stx_barh.json +0 -84
  295. scitex/plt/templates/research-master/scitex/vis/gallery/categorical/stx_box.json +0 -83
  296. scitex/plt/templates/research-master/scitex/vis/gallery/categorical/stx_boxplot.json +0 -93
  297. scitex/plt/templates/research-master/scitex/vis/gallery/categorical/stx_violin.json +0 -91
  298. scitex/plt/templates/research-master/scitex/vis/gallery/categorical/stx_violinplot.json +0 -91
  299. scitex/plt/templates/research-master/scitex/vis/gallery/categorical/violinplot.json +0 -91
  300. scitex/plt/templates/research-master/scitex/vis/gallery/contour/contour.json +0 -97
  301. scitex/plt/templates/research-master/scitex/vis/gallery/contour/contourf.json +0 -98
  302. scitex/plt/templates/research-master/scitex/vis/gallery/contour/stx_contour.json +0 -84
  303. scitex/plt/templates/research-master/scitex/vis/gallery/distribution/hist.json +0 -101
  304. scitex/plt/templates/research-master/scitex/vis/gallery/distribution/hist2d.json +0 -96
  305. scitex/plt/templates/research-master/scitex/vis/gallery/distribution/stx_ecdf.json +0 -95
  306. scitex/plt/templates/research-master/scitex/vis/gallery/distribution/stx_joyplot.json +0 -95
  307. scitex/plt/templates/research-master/scitex/vis/gallery/distribution/stx_kde.json +0 -93
  308. scitex/plt/templates/research-master/scitex/vis/gallery/grid/imshow.json +0 -95
  309. scitex/plt/templates/research-master/scitex/vis/gallery/grid/matshow.json +0 -95
  310. scitex/plt/templates/research-master/scitex/vis/gallery/grid/stx_conf_mat.json +0 -83
  311. scitex/plt/templates/research-master/scitex/vis/gallery/grid/stx_heatmap.json +0 -92
  312. scitex/plt/templates/research-master/scitex/vis/gallery/grid/stx_image.json +0 -121
  313. scitex/plt/templates/research-master/scitex/vis/gallery/grid/stx_imshow.json +0 -84
  314. scitex/plt/templates/research-master/scitex/vis/gallery/line/plot.json +0 -110
  315. scitex/plt/templates/research-master/scitex/vis/gallery/line/step.json +0 -92
  316. scitex/plt/templates/research-master/scitex/vis/gallery/line/stx_line.json +0 -95
  317. scitex/plt/templates/research-master/scitex/vis/gallery/line/stx_shaded_line.json +0 -96
  318. scitex/plt/templates/research-master/scitex/vis/gallery/scatter/hexbin.json +0 -95
  319. scitex/plt/templates/research-master/scitex/vis/gallery/scatter/scatter.json +0 -95
  320. scitex/plt/templates/research-master/scitex/vis/gallery/scatter/stem.json +0 -92
  321. scitex/plt/templates/research-master/scitex/vis/gallery/scatter/stx_scatter.json +0 -84
  322. scitex/plt/templates/research-master/scitex/vis/gallery/special/pie.json +0 -94
  323. scitex/plt/templates/research-master/scitex/vis/gallery/special/stx_raster.json +0 -109
  324. scitex/plt/templates/research-master/scitex/vis/gallery/special/stx_rectangle.json +0 -108
  325. scitex/plt/templates/research-master/scitex/vis/gallery/statistical/errorbar.json +0 -93
  326. scitex/plt/templates/research-master/scitex/vis/gallery/statistical/stx_errorbar.json +0 -84
  327. scitex/plt/templates/research-master/scitex/vis/gallery/statistical/stx_mean_ci.json +0 -96
  328. scitex/plt/templates/research-master/scitex/vis/gallery/statistical/stx_mean_std.json +0 -96
  329. scitex/plt/templates/research-master/scitex/vis/gallery/statistical/stx_median_iqr.json +0 -96
  330. scitex/plt/templates/research-master/scitex/vis/gallery/vector/quiver.json +0 -99
  331. scitex/plt/templates/research-master/scitex/vis/gallery/vector/streamplot.json +0 -100
  332. scitex/vis/__init__.py +0 -177
  333. scitex/vis/editor/_edit.py +0 -390
  334. scitex/vis/editor/flask_editor/_bbox.py +0 -529
  335. scitex/vis/editor/flask_editor/_core.py +0 -168
  336. scitex/vis/editor/flask_editor/_renderer.py +0 -393
  337. scitex/vis/editor/flask_editor/templates/__init__.py +0 -33
  338. scitex/vis/editor/flask_editor/templates/_html.py +0 -513
  339. scitex/vis/editor/flask_editor/templates/_scripts.py +0 -1261
  340. scitex/vis/editor/flask_editor/templates/_styles.py +0 -739
  341. /scitex/{vis → fig}/README.md +0 -0
  342. /scitex/{vis → fig}/backend/__init__.py +0 -0
  343. /scitex/{vis → fig}/backend/_export.py +0 -0
  344. /scitex/{vis → fig}/backend/_render.py +0 -0
  345. /scitex/{vis → fig}/docs/CANVAS_ARCHITECTURE.md +0 -0
  346. /scitex/{vis → fig}/editor/_flask_editor.py +0 -0
  347. /scitex/{vis → fig}/editor/flask_editor/__init__.py +0 -0
  348. /scitex/{vis → fig}/editor/flask_editor/_utils.py +0 -0
  349. /scitex/{vis → fig}/io/_directory.py +0 -0
  350. /scitex/{vis → fig}/model/_plot_types.py +0 -0
  351. /scitex/{vis → fig}/utils/_defaults.py +0 -0
  352. /scitex/{vis → fig}/utils/_validate.py +0 -0
  353. {scitex-2.7.0.dist-info → scitex-2.8.1.dist-info}/WHEEL +0 -0
  354. {scitex-2.7.0.dist-info → scitex-2.8.1.dist-info}/entry_points.txt +0 -0
  355. {scitex-2.7.0.dist-info → scitex-2.8.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,1058 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # Timestamp: "2025-12-14 (ywatanabe)"
4
+ # File: /home/ywatanabe/proj/scitex-code/src/scitex/fig/io/_bundle.py
5
+
6
+ """
7
+ SciTeX .figz Bundle I/O - Figure-specific bundle operations.
8
+
9
+ Handles:
10
+ - Figure specification validation
11
+ - Panel composition and layout
12
+ - Nested .pltz bundle management
13
+ - Export file handling
14
+ """
15
+
16
+ import json
17
+ import shutil
18
+ from pathlib import Path
19
+ from typing import Any, Dict, List
20
+
21
+ __all__ = [
22
+ "validate_figz_spec",
23
+ "load_figz_bundle",
24
+ "save_figz_bundle",
25
+ "FIGZ_SCHEMA_SPEC",
26
+ ]
27
+
28
+ # Schema specification for .figz bundles
29
+ FIGZ_SCHEMA_SPEC = {
30
+ "name": "scitex.fig.figure",
31
+ "version": "1.0.0",
32
+ "required_fields": ["schema"],
33
+ "optional_fields": ["figure", "panels", "notations"],
34
+ }
35
+
36
+
37
+ def validate_figz_spec(spec: Dict[str, Any]) -> List[str]:
38
+ """Validate .figz-specific fields.
39
+
40
+ Args:
41
+ spec: The specification dictionary to validate.
42
+
43
+ Returns:
44
+ List of validation error messages (empty if valid).
45
+ """
46
+ errors = []
47
+
48
+ if "panels" in spec:
49
+ panels = spec["panels"]
50
+ if not isinstance(panels, list):
51
+ errors.append("'panels' must be a list")
52
+ else:
53
+ for i, panel in enumerate(panels):
54
+ if not isinstance(panel, dict):
55
+ errors.append(f"panels[{i}] must be a dictionary")
56
+ continue
57
+ if "id" not in panel:
58
+ errors.append(f"panels[{i}].id is required")
59
+
60
+ if "figure" in spec:
61
+ figure = spec["figure"]
62
+ if not isinstance(figure, dict):
63
+ errors.append("'figure' must be a dictionary")
64
+
65
+ return errors
66
+
67
+
68
+ def load_figz_bundle(bundle_dir: Path) -> Dict[str, Any]:
69
+ """Load .figz bundle contents from directory.
70
+
71
+ Supports both:
72
+ - New format: spec.json + style.json (separate semantic/appearance)
73
+ - Legacy format: {basename}.json (embedded styles)
74
+
75
+ Args:
76
+ bundle_dir: Path to the bundle directory.
77
+
78
+ Returns:
79
+ Dictionary with loaded bundle contents:
80
+ - spec: Figure specification (semantic)
81
+ - style: Figure style (appearance)
82
+ - plots: Dict of nested pltz bundles
83
+ - basename: Base filename
84
+ """
85
+ result = {}
86
+ bundle_dir = Path(bundle_dir)
87
+
88
+ # Determine basename from directory name
89
+ basename = bundle_dir.stem.replace(".figz", "")
90
+ result["basename"] = basename
91
+
92
+ # Try to load spec.json (new format) first
93
+ spec_file = bundle_dir / "spec.json"
94
+ if spec_file.exists():
95
+ with open(spec_file, "r") as f:
96
+ result["spec"] = json.load(f)
97
+ else:
98
+ # Fallback to {basename}.json (legacy format)
99
+ legacy_file = bundle_dir / f"{basename}.json"
100
+ if legacy_file.exists():
101
+ with open(legacy_file, "r") as f:
102
+ result["spec"] = json.load(f)
103
+ else:
104
+ # Try any .json file
105
+ for f in bundle_dir.glob("*.json"):
106
+ if not f.name.startswith('.') and f.name != "style.json":
107
+ with open(f, "r") as fp:
108
+ result["spec"] = json.load(fp)
109
+ break
110
+ else:
111
+ result["spec"] = None
112
+
113
+ # Load style.json if exists
114
+ style_file = bundle_dir / "style.json"
115
+ if style_file.exists():
116
+ with open(style_file, "r") as f:
117
+ result["style"] = json.load(f)
118
+ else:
119
+ # Extract from embedded styles in spec (legacy)
120
+ if result.get("spec"):
121
+ figure = result["spec"].get("figure", {})
122
+ if "styles" in figure:
123
+ result["style"] = figure["styles"]
124
+ else:
125
+ result["style"] = {}
126
+ else:
127
+ result["style"] = {}
128
+
129
+ # Load nested .pltz bundles
130
+ result["plots"] = {}
131
+
132
+ # Load from .pltz.d directories
133
+ for pltz_dir in bundle_dir.glob("*.pltz.d"):
134
+ plot_name = pltz_dir.stem.replace(".pltz", "")
135
+ from scitex.io._bundle import load_bundle
136
+ result["plots"][plot_name] = load_bundle(pltz_dir)
137
+
138
+ # Load from .pltz ZIP files
139
+ for pltz_zip in bundle_dir.glob("*.pltz"):
140
+ if pltz_zip.is_file():
141
+ plot_name = pltz_zip.stem
142
+ from scitex.io._bundle import load_bundle
143
+ result["plots"][plot_name] = load_bundle(pltz_zip)
144
+
145
+ return result
146
+
147
+
148
+ def save_figz_bundle(data: Dict[str, Any], dir_path: Path) -> None:
149
+ """Save .figz bundle contents to directory.
150
+
151
+ Structure:
152
+ figure.figz.d/
153
+ spec.json # Figure-level specification
154
+ style.json # Figure-level style (optional)
155
+ exports/ # Figure-level exports
156
+ figure.png
157
+ figure.svg
158
+ figure_hitmap.png
159
+ figure_overview.png
160
+ cache/ # Figure-level cache
161
+ geometry_px.json # Combined geometry for all panels
162
+ render_manifest.json
163
+ panels/ # Nested panel bundles (or *.pltz.d at root)
164
+ A.pltz.d/
165
+ B.pltz.d/
166
+ README.md
167
+
168
+ Args:
169
+ data: Bundle data dictionary.
170
+ dir_path: Path to the bundle directory.
171
+ """
172
+ import logging
173
+ logger = logging.getLogger("scitex")
174
+
175
+ # Get basename from directory name (e.g., "Figure1" from "Figure1.figz.d")
176
+ basename = dir_path.stem.replace(".figz", "")
177
+
178
+ # Create directories
179
+ exports_dir = dir_path / "exports"
180
+ cache_dir = dir_path / "cache"
181
+ exports_dir.mkdir(parents=True, exist_ok=True)
182
+ cache_dir.mkdir(parents=True, exist_ok=True)
183
+
184
+ # Split spec into spec.json (semantic) and style.json (appearance)
185
+ spec = data.get("spec", {})
186
+ style = data.get("style", {})
187
+
188
+ # Extract style from spec.figure.styles if not provided separately
189
+ figure_data = spec.get("figure", {})
190
+ if not style and "styles" in figure_data:
191
+ style = figure_data.get("styles", {})
192
+
193
+ # Build clean spec (semantic data only)
194
+ clean_spec = {
195
+ "schema": spec.get("schema", {"name": "scitex.fig.figure", "version": "1.0.0"}),
196
+ "figure": {
197
+ "id": figure_data.get("id", "figure"),
198
+ "title": figure_data.get("title", ""),
199
+ "caption": figure_data.get("caption", ""),
200
+ },
201
+ "panels": spec.get("panels", []),
202
+ }
203
+ if "notations" in spec:
204
+ clean_spec["notations"] = spec["notations"]
205
+
206
+ # Build style (appearance data)
207
+ figz_style = {
208
+ "schema": {"name": "scitex.fig.style", "version": "1.0.0"},
209
+ "size": style.get("size", {"width_mm": 180, "height_mm": 120}),
210
+ "background": style.get("background", "#ffffff"),
211
+ "theme": style.get("theme", {"mode": "light"}),
212
+ "panel_labels": style.get("panel_labels", {
213
+ "visible": True,
214
+ "fontsize": 12,
215
+ "fontweight": "bold",
216
+ "position": "top-left",
217
+ }),
218
+ }
219
+
220
+ # Save spec.json (semantic)
221
+ spec_file = dir_path / "spec.json"
222
+ with open(spec_file, "w") as f:
223
+ json.dump(clean_spec, f, indent=2)
224
+
225
+ # Save style.json (appearance)
226
+ style_file = dir_path / "style.json"
227
+ with open(style_file, "w") as f:
228
+ json.dump(figz_style, f, indent=2)
229
+
230
+ # Also save as {basename}.json for backward compatibility (full spec with embedded style)
231
+ compat_spec = dict(clean_spec)
232
+ compat_spec["figure"]["styles"] = {
233
+ "size": figz_style["size"],
234
+ "background": figz_style["background"],
235
+ }
236
+ compat_spec_file = dir_path / f"{basename}.json"
237
+ with open(compat_spec_file, "w") as f:
238
+ json.dump(compat_spec, f, indent=2)
239
+
240
+ # Save exports to exports/ directory
241
+ _save_figz_exports(data, exports_dir, spec, basename)
242
+
243
+ # Copy nested .pltz bundles directly (preserving all files)
244
+ if "plots" in data:
245
+ _copy_nested_pltz_bundles(data["plots"], dir_path)
246
+
247
+ # Generate composed figure in exports/ (Figure1.png, Figure1.svg)
248
+ try:
249
+ _generate_composed_figure(dir_path, spec, basename)
250
+ except Exception as e:
251
+ logger.debug(f"Could not generate composed figure: {e}")
252
+
253
+ # Generate figz overview in exports/
254
+ try:
255
+ _generate_figz_overview(dir_path, spec, data, basename)
256
+ except Exception as e:
257
+ logger.debug(f"Could not generate figz overview: {e}")
258
+
259
+ # Generate figure-level geometry cache
260
+ try:
261
+ _generate_figz_geometry_cache(dir_path, spec, basename)
262
+ except Exception as e:
263
+ logger.debug(f"Could not generate figz geometry cache: {e}")
264
+
265
+ # Generate README.md
266
+ try:
267
+ _generate_figz_readme(dir_path, spec, data, basename)
268
+ except Exception as e:
269
+ logger.debug(f"Could not generate figz README: {e}")
270
+
271
+
272
+ def _save_figz_exports(data: Dict[str, Any], exports_dir: Path, spec: Dict, basename: str) -> None:
273
+ """Save figure-level export files to exports/ directory.
274
+
275
+ Args:
276
+ data: Bundle data containing PNG/SVG/PDF bytes or paths.
277
+ exports_dir: Path to exports/ directory.
278
+ spec: Figure specification.
279
+ basename: Base filename for exports.
280
+ """
281
+ for fmt in ["png", "svg", "pdf"]:
282
+ if fmt not in data:
283
+ continue
284
+
285
+ out_file = exports_dir / f"{basename}.{fmt}"
286
+ export_data = data[fmt]
287
+
288
+ if isinstance(export_data, bytes):
289
+ with open(out_file, "wb") as f:
290
+ f.write(export_data)
291
+ elif isinstance(export_data, (str, Path)) and Path(export_data).exists():
292
+ shutil.copy(export_data, out_file)
293
+
294
+ # Embed metadata into PNG and PDF files
295
+ if out_file.exists() and spec:
296
+ try:
297
+ _embed_metadata_in_export(out_file, spec, fmt)
298
+ except Exception as e:
299
+ import logging
300
+ logging.getLogger("scitex").debug(
301
+ f"Could not embed metadata in {out_file}: {e}"
302
+ )
303
+
304
+
305
+ def _save_exports(data: Dict[str, Any], dir_path: Path, spec: Dict, basename: str = "figure") -> None:
306
+ """Save export files (PNG, SVG, PDF) with embedded metadata. (Legacy - root level)"""
307
+ for fmt in ["png", "svg", "pdf"]:
308
+ if fmt not in data:
309
+ continue
310
+
311
+ out_file = dir_path / f"{basename}.{fmt}"
312
+ export_data = data[fmt]
313
+
314
+ if isinstance(export_data, bytes):
315
+ with open(out_file, "wb") as f:
316
+ f.write(export_data)
317
+ elif isinstance(export_data, (str, Path)) and Path(export_data).exists():
318
+ shutil.copy(export_data, out_file)
319
+
320
+ # Embed metadata into PNG and PDF files
321
+ if out_file.exists() and spec:
322
+ try:
323
+ _embed_metadata_in_export(out_file, spec, fmt)
324
+ except Exception as e:
325
+ import logging
326
+ logging.getLogger("scitex").debug(
327
+ f"Could not embed metadata in {out_file}: {e}"
328
+ )
329
+
330
+
331
+ def _copy_nested_pltz_bundles(plots: Dict[str, Any], dir_path: Path) -> None:
332
+ """Copy nested .pltz bundles directly, preserving all files.
333
+
334
+ Args:
335
+ plots: Dict mapping panel IDs to either:
336
+ - source_path: Path to existing .pltz.d directory or .pltz zip
337
+ - bundle_data: Dict with spec/data (will use save_bundle)
338
+ dir_path: Target figz directory.
339
+ """
340
+ for panel_id, plot_source in plots.items():
341
+ if isinstance(plot_source, (str, Path)):
342
+ # Direct copy from source path
343
+ source_path = Path(plot_source)
344
+
345
+ if source_path.is_dir() and str(source_path).endswith('.pltz.d'):
346
+ # Source is .pltz.d directory - copy as directory
347
+ target_path = dir_path / f"{panel_id}.pltz.d"
348
+ if target_path.exists():
349
+ shutil.rmtree(target_path)
350
+ shutil.copytree(source_path, target_path)
351
+
352
+ elif source_path.is_file() and str(source_path).endswith('.pltz'):
353
+ # Source is .pltz zip file - copy as zip file (preserving zip format)
354
+ target_path = dir_path / f"{panel_id}.pltz"
355
+ if target_path.exists():
356
+ target_path.unlink()
357
+ shutil.copy2(source_path, target_path)
358
+
359
+ elif source_path.exists():
360
+ # Unknown format - try to copy as directory
361
+ target_path = dir_path / f"{panel_id}.pltz.d"
362
+ if source_path.is_dir():
363
+ if target_path.exists():
364
+ shutil.rmtree(target_path)
365
+ shutil.copytree(source_path, target_path)
366
+
367
+ elif isinstance(plot_source, dict):
368
+ # Check if it has source_path for direct copy
369
+ if "source_path" in plot_source:
370
+ source_path = Path(plot_source["source_path"])
371
+ if source_path.is_file() and str(source_path).endswith('.pltz'):
372
+ # .pltz zip file
373
+ target_path = dir_path / f"{panel_id}.pltz"
374
+ if target_path.exists():
375
+ target_path.unlink()
376
+ shutil.copy2(source_path, target_path)
377
+ elif source_path.exists() and source_path.is_dir():
378
+ # .pltz.d directory
379
+ target_path = dir_path / f"{panel_id}.pltz.d"
380
+ if target_path.exists():
381
+ shutil.rmtree(target_path)
382
+ shutil.copytree(source_path, target_path)
383
+ else:
384
+ # Fallback to save_bundle (will lose images)
385
+ from scitex.io._bundle import save_bundle, BundleType
386
+ target_path = dir_path / f"{panel_id}.pltz.d"
387
+ save_bundle(plot_source, target_path, bundle_type=BundleType.PLTZ)
388
+
389
+
390
+ def _generate_figz_overview(dir_path: Path, spec: Dict, data: Dict, basename: str) -> None:
391
+ """Generate overview image for figz bundle showing panels with hitmaps, overlays, and bboxes.
392
+
393
+ Args:
394
+ dir_path: Bundle directory path.
395
+ spec: Bundle specification.
396
+ data: Bundle data dictionary.
397
+ basename: Base filename for bundle files.
398
+ """
399
+ import matplotlib.pyplot as plt
400
+ import matplotlib.gridspec as gridspec
401
+ import matplotlib.patches as patches
402
+ from PIL import Image
403
+ import numpy as np
404
+ import warnings
405
+ import tempfile
406
+ import zipfile
407
+
408
+ # Find all panel bundles (both .pltz.d directories and .pltz zip files)
409
+ panel_dirs = []
410
+ temp_dirs_to_cleanup = []
411
+
412
+ for item in dir_path.iterdir():
413
+ if item.is_dir() and str(item).endswith('.pltz.d'):
414
+ panel_dirs.append(item)
415
+ elif item.is_file() and str(item).endswith('.pltz'):
416
+ # Extract .pltz zip to temp directory for overview generation
417
+ temp_dir = tempfile.mkdtemp(prefix=f'scitex_overview_{item.stem}_')
418
+ temp_dirs_to_cleanup.append(temp_dir)
419
+ with zipfile.ZipFile(item, 'r') as zf:
420
+ zf.extractall(temp_dir)
421
+ # Find the extracted .pltz.d directory
422
+ extracted = Path(temp_dir)
423
+ for subitem in extracted.iterdir():
424
+ if subitem.is_dir() and str(subitem).endswith('.pltz.d'):
425
+ panel_dirs.append(subitem)
426
+ break
427
+ else:
428
+ # Use temp dir directly if no .pltz.d subfolder
429
+ panel_dirs.append(extracted)
430
+
431
+ panel_dirs = sorted(panel_dirs, key=lambda x: x.name)
432
+ n_panels = len(panel_dirs)
433
+
434
+ if n_panels == 0:
435
+ return
436
+
437
+ # Create figure with 2 rows per panel:
438
+ # Row 1: Plot | Hitmap | Overlay
439
+ # Row 2: Bboxes | (empty) | (empty)
440
+ fig_width = 15
441
+ fig_height = 6 * n_panels + 1
442
+ fig = plt.figure(figsize=(fig_width, fig_height), facecolor="white")
443
+
444
+ # Title
445
+ title = spec.get("figure", {}).get("title", basename)
446
+ fig.suptitle(f"Figure Overview: {title}", fontsize=14, fontweight="bold", y=0.99)
447
+
448
+ # Create gridspec - 2 rows per panel, 3 columns
449
+ gs = gridspec.GridSpec(n_panels * 2, 3, figure=fig, hspace=0.3, wspace=0.15,
450
+ height_ratios=[1, 1] * n_panels)
451
+
452
+ # Add each panel
453
+ for idx, panel_dir in enumerate(panel_dirs):
454
+ panel_id = panel_dir.stem.replace(".pltz", "")
455
+ row_base = idx * 2 # Two rows per panel
456
+
457
+ # Find PNG in panel directory (check exports/ first for layered format, then root)
458
+ png_files = list(panel_dir.glob("exports/*.png"))
459
+ if not png_files:
460
+ png_files = list(panel_dir.glob("*.png"))
461
+ main_pngs = [f for f in png_files if "_hitmap" not in f.name and "_overview" not in f.name]
462
+
463
+ # Find hitmap PNG
464
+ hitmap_files = list(panel_dir.glob("exports/*_hitmap.png"))
465
+ if not hitmap_files:
466
+ hitmap_files = list(panel_dir.glob("*_hitmap.png"))
467
+
468
+ # Load geometry for bboxes
469
+ geometry_data = {}
470
+ geometry_path = panel_dir / "cache" / "geometry_px.json"
471
+ if geometry_path.exists():
472
+ with open(geometry_path, "r") as f:
473
+ geometry_data = json.load(f)
474
+
475
+ # === Row 1: Plot | Hitmap | Overlay ===
476
+ # Left subplot: main image
477
+ ax_main = fig.add_subplot(gs[row_base, 0])
478
+ ax_main.set_title(f"Panel {panel_id}", fontweight="bold", fontsize=11)
479
+
480
+ main_img = None
481
+ if main_pngs:
482
+ main_img = Image.open(main_pngs[0])
483
+ ax_main.imshow(main_img)
484
+ else:
485
+ ax_main.text(0.5, 0.5, "No image", ha="center", va="center", transform=ax_main.transAxes)
486
+ ax_main.axis("off")
487
+
488
+ # Middle subplot: hitmap
489
+ ax_hitmap = fig.add_subplot(gs[row_base, 1])
490
+ ax_hitmap.set_title(f"Hitmap {panel_id}", fontweight="bold", fontsize=11)
491
+
492
+ hitmap_img = None
493
+ if hitmap_files:
494
+ hitmap_img = Image.open(hitmap_files[0])
495
+ ax_hitmap.imshow(hitmap_img)
496
+ else:
497
+ ax_hitmap.text(0.5, 0.5, "No hitmap", ha="center", va="center", transform=ax_hitmap.transAxes)
498
+ ax_hitmap.axis("off")
499
+
500
+ # Right subplot: overlay
501
+ ax_overlay = fig.add_subplot(gs[row_base, 2])
502
+ ax_overlay.set_title(f"Overlay {panel_id}", fontweight="bold", fontsize=11)
503
+
504
+ if main_img is not None:
505
+ ax_overlay.imshow(main_img)
506
+ if hitmap_img is not None:
507
+ hitmap_rgba = hitmap_img.convert("RGBA")
508
+ hitmap_array = np.array(hitmap_rgba)
509
+ # Create semi-transparent overlay
510
+ hitmap_array[:, :, 3] = (hitmap_array[:, :, 3] * 0.5).astype(np.uint8)
511
+ ax_overlay.imshow(hitmap_array, alpha=0.5)
512
+ else:
513
+ ax_overlay.text(0.5, 0.5, "No overlay", ha="center", va="center", transform=ax_overlay.transAxes)
514
+ ax_overlay.axis("off")
515
+
516
+ # === Row 2: Bboxes ===
517
+ ax_bboxes = fig.add_subplot(gs[row_base + 1, 0])
518
+ ax_bboxes.set_title(f"Bboxes {panel_id}", fontweight="bold", fontsize=11)
519
+
520
+ if main_img is not None:
521
+ ax_bboxes.imshow(main_img)
522
+ # Draw bboxes from geometry
523
+ _draw_bboxes_from_geometry(ax_bboxes, geometry_data)
524
+ else:
525
+ ax_bboxes.text(0.5, 0.5, "No image", ha="center", va="center", transform=ax_bboxes.transAxes)
526
+ ax_bboxes.axis("off")
527
+
528
+ # Info panel
529
+ ax_info = fig.add_subplot(gs[row_base + 1, 1:])
530
+ ax_info.set_title(f"Info {panel_id}", fontweight="bold", fontsize=11)
531
+ ax_info.axis("off")
532
+
533
+ # Show spec/style summary
534
+ spec_path = panel_dir / "spec.json"
535
+ style_path = panel_dir / "style.json"
536
+ info_text = ""
537
+
538
+ if spec_path.exists():
539
+ with open(spec_path, "r") as f:
540
+ spec_data = json.load(f)
541
+ info_text += f"Axes: {len(spec_data.get('axes', []))}\n"
542
+ info_text += f"Traces: {len(spec_data.get('traces', []))}\n"
543
+
544
+ if style_path.exists():
545
+ with open(style_path, "r") as f:
546
+ style_data = json.load(f)
547
+ size = style_data.get("size", {})
548
+ info_text += f"Size: {size.get('width_mm', 0):.1f} × {size.get('height_mm', 0):.1f} mm\n"
549
+ info_text += f"Theme: {style_data.get('theme', {}).get('mode', 'light')}\n"
550
+
551
+ manifest_path = panel_dir / "cache" / "render_manifest.json"
552
+ if manifest_path.exists():
553
+ with open(manifest_path, "r") as f:
554
+ manifest_data = json.load(f)
555
+ info_text += f"DPI: {manifest_data.get('dpi', 300)}\n"
556
+ render_px = manifest_data.get("render_px", [0, 0])
557
+ info_text += f"Pixels: {render_px[0]} × {render_px[1]}\n"
558
+
559
+ ax_info.text(0.02, 0.98, info_text, transform=ax_info.transAxes,
560
+ fontsize=10, fontfamily="monospace", verticalalignment="top",
561
+ bbox=dict(boxstyle="round", facecolor="wheat", alpha=0.5))
562
+
563
+ # Save overview to exports/ directory
564
+ exports_dir = dir_path / "exports"
565
+ exports_dir.mkdir(parents=True, exist_ok=True)
566
+ overview_path = exports_dir / f"{basename}_overview.png"
567
+ with warnings.catch_warnings():
568
+ warnings.filterwarnings("ignore", message=".*tight_layout.*")
569
+ fig.savefig(overview_path, dpi=150, bbox_inches="tight", facecolor="white")
570
+ plt.close(fig)
571
+
572
+ # Cleanup temp directories
573
+ for temp_dir in temp_dirs_to_cleanup:
574
+ try:
575
+ shutil.rmtree(temp_dir)
576
+ except Exception:
577
+ pass
578
+
579
+
580
+ def _draw_bboxes_from_geometry(ax, geometry_data: Dict) -> None:
581
+ """Draw bboxes from geometry data on an axes.
582
+
583
+ Args:
584
+ ax: Matplotlib axes.
585
+ geometry_data: Geometry data dictionary.
586
+ """
587
+ import matplotlib.patches as patches
588
+
589
+ colors = ["red", "blue", "green", "orange", "purple", "cyan"]
590
+ selectable = geometry_data.get("selectable_regions", {})
591
+
592
+ for ax_idx, ax_region in enumerate(selectable.get("axes", [])):
593
+ color = colors[ax_idx % len(colors)]
594
+
595
+ # Title bbox
596
+ if "title" in ax_region:
597
+ bbox = ax_region["title"].get("bbox_px", [])
598
+ if len(bbox) == 4:
599
+ _draw_single_bbox(ax, bbox, color, "title")
600
+
601
+ # xlabel bbox
602
+ if "xlabel" in ax_region:
603
+ bbox = ax_region["xlabel"].get("bbox_px", [])
604
+ if len(bbox) == 4:
605
+ _draw_single_bbox(ax, bbox, color, "xlabel")
606
+
607
+ # ylabel bbox
608
+ if "ylabel" in ax_region:
609
+ bbox = ax_region["ylabel"].get("bbox_px", [])
610
+ if len(bbox) == 4:
611
+ _draw_single_bbox(ax, bbox, color, "ylabel")
612
+
613
+ # xaxis spine
614
+ if "xaxis" in ax_region and "spine" in ax_region["xaxis"]:
615
+ bbox = ax_region["xaxis"]["spine"].get("bbox_px", [])
616
+ if len(bbox) == 4:
617
+ _draw_single_bbox(ax, bbox, "gray", "xaxis", lw=1)
618
+
619
+ # yaxis spine
620
+ if "yaxis" in ax_region and "spine" in ax_region["yaxis"]:
621
+ bbox = ax_region["yaxis"]["spine"].get("bbox_px", [])
622
+ if len(bbox) == 4:
623
+ _draw_single_bbox(ax, bbox, "gray", "yaxis", lw=1)
624
+
625
+ # legend bbox
626
+ if "legend" in ax_region:
627
+ bbox = ax_region["legend"].get("bbox_px", [])
628
+ if len(bbox) == 4:
629
+ _draw_single_bbox(ax, bbox, "magenta", "legend")
630
+
631
+
632
+ def _draw_single_bbox(ax, bbox: List, color: str, label: str, lw: int = 2) -> None:
633
+ """Draw a single bbox rectangle on axes.
634
+
635
+ Args:
636
+ ax: Matplotlib axes.
637
+ bbox: [x0, y0, x1, y1] bounding box (corner coordinates).
638
+ color: Rectangle color.
639
+ label: Label text.
640
+ lw: Line width.
641
+ """
642
+ import matplotlib.patches as patches
643
+
644
+ # bbox is [x0, y0, x1, y1] format
645
+ x0, y0, x1, y1 = bbox
646
+ width = x1 - x0
647
+ height = y1 - y0
648
+ rect = patches.Rectangle((x0, y0), width, height,
649
+ linewidth=lw, edgecolor=color, facecolor='none')
650
+ ax.add_patch(rect)
651
+ # Add label
652
+ ax.text(x0 + 2, y0 + height / 2, label, fontsize=6, color=color, fontweight="bold")
653
+
654
+
655
+ def _generate_composed_figure(dir_path: Path, spec: Dict, basename: str) -> None:
656
+ """Generate composed figure from panel images.
657
+
658
+ Composes all panel PNG images into a single figure based on the layout
659
+ specified in the figz spec.
660
+
661
+ Args:
662
+ dir_path: Bundle directory path.
663
+ spec: Bundle specification with panel layout.
664
+ basename: Base filename for exports.
665
+ """
666
+ from PIL import Image
667
+ import warnings
668
+
669
+ exports_dir = dir_path / "exports"
670
+ exports_dir.mkdir(parents=True, exist_ok=True)
671
+
672
+ # Load style from style.json if exists, else from spec
673
+ style_file = dir_path / "style.json"
674
+ if style_file.exists():
675
+ with open(style_file, "r") as f:
676
+ style = json.load(f)
677
+ size = style.get("size", {})
678
+ background = style.get("background", "#ffffff")
679
+ else:
680
+ # Fallback to embedded styles in spec
681
+ figure = spec.get("figure", {})
682
+ styles = figure.get("styles", {})
683
+ size = styles.get("size", {})
684
+ background = styles.get("background", "#ffffff")
685
+
686
+ fig_width_mm = size.get("width_mm", 180)
687
+ fig_height_mm = size.get("height_mm", 120)
688
+
689
+ # Use 300 DPI for composition
690
+ dpi = 300
691
+ mm_to_inch = 1 / 25.4
692
+ fig_width_px = int(fig_width_mm * mm_to_inch * dpi)
693
+ fig_height_px = int(fig_height_mm * mm_to_inch * dpi)
694
+
695
+ # Create canvas
696
+ canvas = Image.new("RGB", (fig_width_px, fig_height_px), background)
697
+
698
+ # Get panels from spec
699
+ panels = spec.get("panels", [])
700
+
701
+ for panel in panels:
702
+ panel_id = panel.get("id", "")
703
+ plot_ref = panel.get("plot", "")
704
+
705
+ # Find the panel's pltz bundle
706
+ if plot_ref.endswith(".pltz.d"):
707
+ panel_dir = dir_path / plot_ref
708
+ else:
709
+ panel_dir = dir_path / f"{panel_id}.pltz.d"
710
+
711
+ if not panel_dir.exists():
712
+ continue
713
+
714
+ # Find panel PNG in exports/
715
+ panel_png = None
716
+ exports_subdir = panel_dir / "exports"
717
+ if exports_subdir.exists():
718
+ for png_file in exports_subdir.glob("*.png"):
719
+ if "_hitmap" not in png_file.name and "_overview" not in png_file.name:
720
+ panel_png = png_file
721
+ break
722
+
723
+ # Fallback: look in panel root
724
+ if not panel_png:
725
+ for png_file in panel_dir.glob("*.png"):
726
+ if "_hitmap" not in png_file.name and "_overview" not in png_file.name:
727
+ panel_png = png_file
728
+ break
729
+
730
+ if not panel_png or not panel_png.exists():
731
+ continue
732
+
733
+ # Load panel image
734
+ panel_img = Image.open(panel_png)
735
+
736
+ # Get panel position and size from spec
737
+ pos = panel.get("position", {})
738
+ panel_size = panel.get("size", {})
739
+
740
+ x_mm = pos.get("x_mm", 0)
741
+ y_mm = pos.get("y_mm", 0)
742
+ width_mm = panel_size.get("width_mm", 80)
743
+ height_mm = panel_size.get("height_mm", 68)
744
+
745
+ # Convert to pixels
746
+ x_px = int(x_mm * mm_to_inch * dpi)
747
+ y_px = int(y_mm * mm_to_inch * dpi)
748
+ target_width = int(width_mm * mm_to_inch * dpi)
749
+ target_height = int(height_mm * mm_to_inch * dpi)
750
+
751
+ # Resize panel to fit
752
+ panel_img = panel_img.resize((target_width, target_height), Image.Resampling.LANCZOS)
753
+
754
+ # Convert to RGB if necessary (for transparent PNGs)
755
+ if panel_img.mode == "RGBA":
756
+ # Create white background
757
+ bg = Image.new("RGB", panel_img.size, background)
758
+ bg.paste(panel_img, mask=panel_img.split()[3])
759
+ panel_img = bg
760
+ elif panel_img.mode != "RGB":
761
+ panel_img = panel_img.convert("RGB")
762
+
763
+ # Paste onto canvas
764
+ canvas.paste(panel_img, (x_px, y_px))
765
+
766
+ # Save composed figure
767
+ png_path = exports_dir / f"{basename}.png"
768
+ canvas.save(png_path, "PNG", dpi=(dpi, dpi))
769
+
770
+ # Also save as SVG (embed PNG in SVG for now)
771
+ svg_path = exports_dir / f"{basename}.svg"
772
+ svg_width_in = fig_width_mm * mm_to_inch
773
+ svg_height_in = fig_height_mm * mm_to_inch
774
+
775
+ # Create simple SVG wrapper with embedded image
776
+ import base64
777
+ with open(png_path, "rb") as f:
778
+ png_b64 = base64.b64encode(f.read()).decode("utf-8")
779
+
780
+ svg_content = f'''<?xml version="1.0" encoding="UTF-8"?>
781
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
782
+ width="{fig_width_px}" height="{fig_height_px}"
783
+ viewBox="0 0 {fig_width_px} {fig_height_px}">
784
+ <image width="{fig_width_px}" height="{fig_height_px}"
785
+ xlink:href="data:image/png;base64,{png_b64}"/>
786
+ </svg>'''
787
+
788
+ with open(svg_path, "w") as f:
789
+ f.write(svg_content)
790
+
791
+
792
+ def _generate_figz_geometry_cache(dir_path: Path, spec: Dict, basename: str) -> None:
793
+ """Generate figure-level geometry cache combining all panel geometries.
794
+
795
+ Creates:
796
+ cache/geometry_px.json - Combined geometry for all panels
797
+ cache/render_manifest.json - Figure-level render metadata
798
+
799
+ Args:
800
+ dir_path: Bundle directory path.
801
+ spec: Bundle specification.
802
+ basename: Base filename for bundle files.
803
+ """
804
+ from datetime import datetime
805
+ import tempfile
806
+ import zipfile
807
+
808
+ cache_dir = dir_path / "cache"
809
+ cache_dir.mkdir(parents=True, exist_ok=True)
810
+
811
+ # Collect geometry from all panel bundles
812
+ combined_geometry = {
813
+ "figure_id": basename,
814
+ "panels": {},
815
+ "generated_at": datetime.now().isoformat(),
816
+ }
817
+
818
+ # Find all panel bundles (both .pltz.d directories and .pltz zip files)
819
+ panel_sources = []
820
+ temp_dirs_to_cleanup = []
821
+
822
+ for item in dir_path.iterdir():
823
+ if item.is_dir() and str(item).endswith('.pltz.d'):
824
+ panel_sources.append((item.stem.replace('.pltz', ''), item))
825
+ elif item.is_file() and str(item).endswith('.pltz'):
826
+ # Extract .pltz zip to temp directory
827
+ temp_dir = tempfile.mkdtemp(prefix=f'scitex_geom_{item.stem}_')
828
+ temp_dirs_to_cleanup.append(temp_dir)
829
+ with zipfile.ZipFile(item, 'r') as zf:
830
+ zf.extractall(temp_dir)
831
+ extracted = Path(temp_dir)
832
+ for subitem in extracted.iterdir():
833
+ if subitem.is_dir() and str(subitem).endswith('.pltz.d'):
834
+ panel_sources.append((item.stem, subitem))
835
+ break
836
+ else:
837
+ panel_sources.append((item.stem, extracted))
838
+
839
+ panel_sources = sorted(panel_sources, key=lambda x: x[0])
840
+
841
+ for panel_id, panel_dir in panel_sources:
842
+
843
+ # Load panel geometry
844
+ panel_geometry_path = panel_dir / "cache" / "geometry_px.json"
845
+ if panel_geometry_path.exists():
846
+ with open(panel_geometry_path, "r") as f:
847
+ panel_geometry = json.load(f)
848
+ combined_geometry["panels"][panel_id] = panel_geometry
849
+
850
+ # Add panel positions from spec
851
+ panels_spec = spec.get("panels", [])
852
+ for panel in panels_spec:
853
+ panel_id = panel.get("id")
854
+ if panel_id and panel_id in combined_geometry["panels"]:
855
+ combined_geometry["panels"][panel_id]["position_mm"] = panel.get("position", {})
856
+ combined_geometry["panels"][panel_id]["size_mm"] = panel.get("size", {})
857
+
858
+ # Save combined geometry
859
+ geometry_path = cache_dir / "geometry_px.json"
860
+ with open(geometry_path, "w") as f:
861
+ json.dump(combined_geometry, f, indent=2)
862
+
863
+ # Generate render manifest
864
+ figure_styles = spec.get("figure", {}).get("styles", {})
865
+ size = figure_styles.get("size", {})
866
+
867
+ manifest = {
868
+ "figure_id": basename,
869
+ "generated_at": datetime.now().isoformat(),
870
+ "size_mm": [size.get("width_mm", 0), size.get("height_mm", 0)],
871
+ "panels_count": len(panel_sources),
872
+ "schema": spec.get("schema", {}),
873
+ }
874
+
875
+ manifest_path = cache_dir / "render_manifest.json"
876
+ with open(manifest_path, "w") as f:
877
+ json.dump(manifest, f, indent=2)
878
+
879
+ # Cleanup temp directories
880
+ for temp_dir in temp_dirs_to_cleanup:
881
+ try:
882
+ shutil.rmtree(temp_dir)
883
+ except Exception:
884
+ pass
885
+
886
+
887
+ def _embed_metadata_in_export(
888
+ file_path: Path, spec: Dict[str, Any], fmt: str
889
+ ) -> None:
890
+ """Embed bundle spec metadata into exported image files."""
891
+ from scitex.io._metadata import embed_metadata
892
+
893
+ embed_data = {
894
+ "scitex_bundle": True,
895
+ "schema": spec.get("schema", {}),
896
+ }
897
+
898
+ for key in ["figure", "panels", "notations"]:
899
+ if key in spec:
900
+ embed_data[key] = spec[key]
901
+
902
+ if fmt in ("png", "pdf"):
903
+ embed_metadata(str(file_path), embed_data)
904
+
905
+
906
+ def _generate_figz_readme(
907
+ dir_path: Path, spec: Dict, data: Dict, basename: str
908
+ ) -> None:
909
+ """Generate a dynamic README.md for figz bundle.
910
+
911
+ Args:
912
+ dir_path: Bundle directory path.
913
+ spec: Bundle specification.
914
+ data: Bundle data dictionary.
915
+ basename: Base filename for bundle files.
916
+ """
917
+ from datetime import datetime
918
+
919
+ # Extract figure info
920
+ figure = spec.get("figure", {})
921
+ title = figure.get("title", basename)
922
+ caption = figure.get("caption", "")
923
+
924
+ # Load style from style.json if exists, else from spec.figure.styles
925
+ style_file = dir_path / "style.json"
926
+ if style_file.exists():
927
+ with open(style_file, "r") as f:
928
+ style = json.load(f)
929
+ size = style.get("size", {})
930
+ background = style.get("background", "#ffffff")
931
+ else:
932
+ styles = figure.get("styles", {})
933
+ size = styles.get("size", {})
934
+ background = styles.get("background", "#ffffff")
935
+
936
+ width_mm = size.get("width_mm", 0)
937
+ height_mm = size.get("height_mm", 0)
938
+
939
+ # Count panels
940
+ panels = spec.get("panels", [])
941
+ n_panels = len(panels)
942
+
943
+ # Find panel directories
944
+ panel_dirs = sorted(dir_path.glob("*.pltz.d"))
945
+
946
+ # Build panel table
947
+ panel_rows = ""
948
+ for panel in panels:
949
+ panel_id = panel.get("id", "?")
950
+ label = panel.get("label", panel_id)
951
+ plot_ref = panel.get("plot", "")
952
+ pos = panel.get("position", {})
953
+ panel_size = panel.get("size", {})
954
+ panel_rows += f"| {label} | {plot_ref} | ({pos.get('x_mm', 0)}, {pos.get('y_mm', 0)}) | {panel_size.get('width_mm', 0)} × {panel_size.get('height_mm', 0)} mm |\n"
955
+
956
+ # Build panel directory list
957
+ panel_dir_list = ""
958
+ for pd in panel_dirs:
959
+ panel_dir_list += f"│ ├── {pd.name}/\n"
960
+
961
+ readme_content = f"""# {basename}.figz.d
962
+
963
+ > SciTeX Figure Bundle - Auto-generated README
964
+
965
+ ## Overview
966
+
967
+ ![Figure Overview](exports/{basename}_overview.png)
968
+
969
+ ## Bundle Structure
970
+
971
+ ```
972
+ {basename}.figz.d/
973
+ ├── spec.json # Figure specification (semantic: what to draw)
974
+ ├── style.json # Figure style (appearance: how it looks)
975
+ ├── {basename}.json # Combined spec+style (legacy compatibility)
976
+ ├── exports/ # Figure-level exports
977
+ │ ├── {basename}.png # Rendered figure (raster)
978
+ │ ├── {basename}.svg # Rendered figure (vector)
979
+ │ └── {basename}_overview.png # Visual summary with hitmaps
980
+ ├── cache/ # Figure-level cache (regenerable)
981
+ │ ├── geometry_px.json # Combined geometry for all panels
982
+ │ └── render_manifest.json # Render metadata
983
+ {panel_dir_list}└── README.md # This file
984
+ ```
985
+
986
+ ## Figure Information
987
+
988
+ | Property | Value |
989
+ |----------|-------|
990
+ | Title | {title or '(none)'} |
991
+ | Panels | {n_panels} |
992
+ | Size | {width_mm:.1f} × {height_mm:.1f} mm |
993
+ | Background | `{background}` |
994
+
995
+ {f"**Caption**: {caption}" if caption else ""}
996
+
997
+ ## Panel Layout
998
+
999
+ | Label | Plot Bundle | Position (x, y) | Size |
1000
+ |-------|-------------|-----------------|------|
1001
+ {panel_rows}
1002
+
1003
+ ## Nested Bundles
1004
+
1005
+ Each panel is stored as a separate `.pltz.d` bundle containing:
1006
+ - `spec.json` - What to plot (data, axes, traces)
1007
+ - `style.json` - How it looks (colors, fonts, theme)
1008
+ - `exports/` - Rendered images (PNG, SVG, hitmap)
1009
+ - `cache/` - Computed geometry (regenerable)
1010
+
1011
+ ## Usage
1012
+
1013
+ ### Python
1014
+
1015
+ ```python
1016
+ import scitex as stx
1017
+
1018
+ # Load the figure bundle
1019
+ bundle = stx.load("{dir_path}")
1020
+
1021
+ # Access components
1022
+ spec = bundle["spec"] # Figure layout
1023
+ plots = bundle["plots"] # Dict of panel bundles
1024
+
1025
+ # Access specific panel
1026
+ panel_a = plots["A"] # Get panel A's pltz bundle
1027
+ ```
1028
+
1029
+ ### Editing
1030
+
1031
+ Edit `spec.json` to change semantic content:
1032
+ - Panel positions and sizes
1033
+ - Figure title and caption
1034
+ - Panel layout
1035
+
1036
+ Edit `style.json` to change appearance:
1037
+ - Figure size (width_mm, height_mm)
1038
+ - Background color
1039
+ - Panel label styling
1040
+ - Theme (light/dark)
1041
+
1042
+ Edit individual `*.pltz.d/spec.json` and `*.pltz.d/style.json` to change:
1043
+ - Plot data and axes (spec.json)
1044
+ - Trace specifications (spec.json)
1045
+ - Colors, fonts, theme (style.json)
1046
+
1047
+ ---
1048
+
1049
+ *Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*
1050
+ *Schema: {spec.get("schema", {}).get("name", "scitex.fig.figure")} v{spec.get("schema", {}).get("version", "1.0.0")}*
1051
+ """
1052
+
1053
+ readme_path = dir_path / "README.md"
1054
+ with open(readme_path, "w") as f:
1055
+ f.write(readme_content)
1056
+
1057
+
1058
+ # EOF