scitex 2.10.3__py3-none-any.whl → 2.11.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (363) hide show
  1. scitex/__init__.py +1 -4
  2. scitex/__version__.py +1 -1
  3. scitex/_install_guide.py +14 -2
  4. scitex/bridge/_figrecipe.py +1 -1
  5. scitex/bridge/_helpers.py +1 -1
  6. scitex/bridge/_plt_vis.py +1 -1
  7. scitex/bridge/_stats_plt.py +1 -1
  8. scitex/bridge/_stats_vis.py +2 -2
  9. scitex/{fig → canvas}/__init__.py +84 -96
  10. scitex/{fig → canvas}/backend/_parser.py +1 -1
  11. scitex/{fig → canvas}/canvas.py +13 -14
  12. scitex/{fts/_fig/_editor → canvas/editor}/_defaults.py +2 -2
  13. scitex/{fig → canvas}/editor/edit/__init__.py +11 -14
  14. scitex/{fig → canvas}/editor/edit/bundle_resolver.py +56 -48
  15. scitex/{fig → canvas}/editor/edit/editor_launcher.py +79 -26
  16. scitex/{fts/_fig/_editor/_cui/_panel_loader.py → canvas/editor/edit/panel_loader.py} +8 -8
  17. scitex/{fts/_fig/_editor/_gui/_flask_editor → canvas/editor/flask_editor}/_bbox.py +2 -1
  18. scitex/{fts/_fig/_editor/_gui/_flask_editor → canvas/editor/flask_editor}/_core.py +84 -84
  19. scitex/{fts/_fig/_editor/_gui/_flask_editor → canvas/editor/flask_editor}/_renderer.py +7 -6
  20. scitex/{fts/_fig/_editor/_gui/_flask_editor → canvas/editor/flask_editor}/static/css/features/canvas.css +2 -2
  21. scitex/{fig → canvas}/editor/flask_editor/static/css/features/panel-grid.css +1 -1
  22. scitex/{fig → canvas}/editor/flask_editor/static/js/core/api.js +3 -4
  23. scitex/{fig → canvas}/editor/flask_editor/static/js/editor/preview.js +5 -5
  24. scitex/{fig → canvas}/editor/flask_editor/templates/_html.py +3 -3
  25. scitex/{fig → canvas}/editor/flask_editor/templates/_scripts.py +10 -10
  26. scitex/{fig → canvas}/editor/flask_editor/templates/_styles.py +3 -3
  27. scitex/{fig → canvas}/io/__init__.py +32 -38
  28. scitex/{fig → canvas}/io/_bundle.py +217 -154
  29. scitex/{fig → canvas}/io/_canvas.py +1 -1
  30. scitex/{fig → canvas}/io/_data.py +1 -1
  31. scitex/{fig → canvas}/io/_export.py +1 -1
  32. scitex/{fig → canvas}/io/_load.py +1 -1
  33. scitex/{fig → canvas}/io/_panel.py +1 -1
  34. scitex/{fig → canvas}/io/_save.py +1 -1
  35. scitex/{fig → canvas}/model/__init__.py +1 -1
  36. scitex/{fig → canvas}/model/_annotations.py +1 -1
  37. scitex/{fig → canvas}/model/_axes.py +1 -1
  38. scitex/{fig → canvas}/model/_figure.py +1 -1
  39. scitex/{fig → canvas}/model/_guides.py +1 -1
  40. scitex/{fig → canvas}/model/_plot.py +1 -1
  41. scitex/{fig → canvas}/model/_styles.py +1 -1
  42. scitex/{fig → canvas}/utils/__init__.py +1 -1
  43. scitex/cli/convert.py +10 -6
  44. scitex/diagram/README.md +7 -7
  45. scitex/io/__init__.py +7 -19
  46. scitex/io/_load.py +15 -19
  47. scitex/io/_load_modules/_canvas.py +2 -2
  48. scitex/io/_load_modules/_con.py +5 -5
  49. scitex/io/_load_modules/_eeg.py +16 -12
  50. scitex/io/_save.py +11 -16
  51. scitex/io/_save_modules/__init__.py +6 -10
  52. scitex/io/_save_modules/_canvas.py +3 -3
  53. scitex/io/_save_modules/_plot_bundle.py +112 -0
  54. scitex/io/_save_modules/{_pltz_stx.py → _plot_scitex.py} +7 -7
  55. scitex/io/_save_modules/_stx_bundle.py +16 -16
  56. scitex/io/bundle/README.md +89 -80
  57. scitex/{fts/_bundle/_FTS.py → io/bundle/_Bundle.py} +197 -95
  58. scitex/io/bundle/__init__.py +67 -35
  59. scitex/{fts/_bundle → io/bundle}/_children.py +32 -40
  60. scitex/io/bundle/_core.py +184 -97
  61. scitex/{fts/_bundle/_dataclasses/_Node.py → io/bundle/_dataclasses/_Spec.py} +29 -23
  62. scitex/{fts/_bundle/_dataclasses/_NodeRefs.py → io/bundle/_dataclasses/_SpecRefs.py} +6 -6
  63. scitex/{fts/_bundle → io/bundle}/_dataclasses/__init__.py +4 -4
  64. scitex/{fts/_bundle → io/bundle}/_loader.py +19 -19
  65. scitex/io/bundle/_manifest.py +99 -0
  66. scitex/{fts/_bundle → io/bundle}/_mpl_helpers.py +119 -28
  67. scitex/io/bundle/_nested.py +113 -100
  68. scitex/{fts/_bundle → io/bundle}/_saver.py +13 -14
  69. scitex/{fts/_bundle → io/bundle}/_storage.py +3 -3
  70. scitex/io/bundle/_types.py +41 -16
  71. scitex/{fts/_bundle → io/bundle}/_validation.py +20 -18
  72. scitex/io/bundle/_zip.py +21 -31
  73. scitex/{fts/_kinds → io/bundle/kinds}/_plot/_backend/_parser.py +1 -1
  74. scitex/{fts/_kinds → io/bundle/kinds}/_plot/_models/_Annotations.py +1 -1
  75. scitex/{fts/_kinds → io/bundle/kinds}/_plot/_models/_Axes.py +1 -1
  76. scitex/{fts/_kinds → io/bundle/kinds}/_plot/_models/_Figure.py +1 -1
  77. scitex/{fts/_fig → io/bundle/kinds/_plot}/_models/_Guides.py +1 -1
  78. scitex/{fts/_kinds → io/bundle/kinds}/_plot/_models/_Plot.py +1 -1
  79. scitex/{fts/_fig → io/bundle/kinds/_plot}/_models/_Styles.py +1 -1
  80. scitex/{fts/_kinds → io/bundle/kinds}/_plot/_utils/_plot_layout.py +1 -1
  81. scitex/{fts/_kinds → io/bundle/kinds}/_table/_latex/__init__.py +1 -1
  82. scitex/{fts/_kinds → io/bundle/kinds}/_table/_latex/_editor/_app.py +1 -1
  83. scitex/{fts/_tables → io/bundle/kinds/_table}/_latex/_export.py +1 -1
  84. scitex/{fts/_kinds → io/bundle/kinds}/_table/_latex/_figure_exporter.py +1 -1
  85. scitex/{fts/_kinds → io/bundle/kinds}/_table/_latex/_table_exporter.py +1 -1
  86. scitex/io/bundle/schemas/__init__.py +30 -0
  87. scitex/parallel/_run.py +5 -4
  88. scitex/path/_find.py +60 -83
  89. scitex/path/_get_module_path.py +23 -21
  90. scitex/path/_get_spath.py +6 -27
  91. scitex/path/_getsize.py +23 -9
  92. scitex/path/_increment_version.py +31 -38
  93. scitex/path/_mk_spath.py +26 -29
  94. scitex/path/_path.py +5 -12
  95. scitex/path/_split.py +27 -15
  96. scitex/path/_this_path.py +23 -9
  97. scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin/__init__.py +2 -1
  98. scitex/plt/_subplots/_AxisWrapperMixins/__init__.py +2 -2
  99. scitex/plt/gallery/_generate.py +76 -50
  100. scitex/plt/io/__init__.py +17 -19
  101. scitex/plt/io/_bundle.py +99 -52
  102. scitex/plt/io/_layered_bundle.py +303 -168
  103. scitex/plt/utils/_csv_column_naming.py +250 -118
  104. scitex/schema/__init__.py +69 -73
  105. scitex/schema/_canvas.py +1 -1
  106. scitex/schema/_stats.py +2 -2
  107. scitex/stats/__init__.py +30 -33
  108. scitex/stats/_schema.py +1 -1
  109. scitex/stats/io/__init__.py +10 -11
  110. scitex/stats/io/_bundle.py +16 -16
  111. {scitex-2.10.3.dist-info → scitex-2.11.0.dist-info}/METADATA +190 -73
  112. {scitex-2.10.3.dist-info → scitex-2.11.0.dist-info}/RECORD +237 -360
  113. scitex/fig/editor/_defaults.py +0 -300
  114. scitex/fig/editor/edit/panel_loader.py +0 -232
  115. scitex/fig/editor/flask_editor/_bbox.py +0 -1299
  116. scitex/fig/editor/flask_editor/_core.py +0 -1429
  117. scitex/fig/editor/flask_editor/_renderer.py +0 -813
  118. scitex/fig/editor/flask_editor/static/css/features/canvas.css +0 -176
  119. scitex/fts/README.md +0 -262
  120. scitex/fts/TODO.md +0 -66
  121. scitex/fts/__init__.py +0 -90
  122. scitex/fts/_bundle/README_IN_BUNDLE.md +0 -102
  123. scitex/fts/_bundle/__init__.py +0 -38
  124. scitex/fts/_bundle/_utils/__init__.py +0 -55
  125. scitex/fts/_bundle/_utils/_const.py +0 -26
  126. scitex/fts/_bundle/_utils/_errors.py +0 -73
  127. scitex/fts/_bundle/_utils/_generate.py +0 -21
  128. scitex/fts/_bundle/_utils/_types.py +0 -76
  129. scitex/fts/_bundle/_zipbundle.py +0 -165
  130. scitex/fts/_fig/__init__.py +0 -22
  131. scitex/fts/_fig/_backend/_parser.py +0 -188
  132. scitex/fts/_fig/_editor/__init__.py +0 -14
  133. scitex/fts/_fig/_editor/_cui/__init__.py +0 -33
  134. scitex/fts/_fig/_editor/_cui/_backend_detector.py +0 -39
  135. scitex/fts/_fig/_editor/_cui/_bundle_resolver.py +0 -366
  136. scitex/fts/_fig/_editor/_cui/_editor_launcher.py +0 -175
  137. scitex/fts/_fig/_editor/_cui/_manual_handler.py +0 -52
  138. scitex/fts/_fig/_editor/_cui/_path_resolver.py +0 -66
  139. scitex/fts/_fig/_editor/_gui/__init__.py +0 -11
  140. scitex/fts/_fig/_editor/_gui/_flask_editor/__init__.py +0 -20
  141. scitex/fts/_fig/_editor/_gui/_flask_editor/_plotter.py +0 -664
  142. scitex/fts/_fig/_editor/_gui/_flask_editor/_utils.py +0 -79
  143. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/base/reset.css +0 -41
  144. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/base/typography.css +0 -16
  145. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/base/variables.css +0 -85
  146. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/components/buttons.css +0 -217
  147. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/components/context-menu.css +0 -93
  148. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/components/dropdown.css +0 -57
  149. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/components/forms.css +0 -112
  150. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/components/modal.css +0 -59
  151. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/components/sections.css +0 -212
  152. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/features/element-inspector.css +0 -190
  153. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/features/loading.css +0 -59
  154. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/features/overlay.css +0 -45
  155. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/features/panel-grid.css +0 -95
  156. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/features/selection.css +0 -101
  157. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/features/statistics.css +0 -138
  158. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/index.css +0 -31
  159. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/layout/container.css +0 -7
  160. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/layout/controls.css +0 -56
  161. scitex/fts/_fig/_editor/_gui/_flask_editor/static/css/layout/preview.css +0 -78
  162. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/alignment/axis.js +0 -314
  163. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/alignment/basic.js +0 -107
  164. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/alignment/distribute.js +0 -54
  165. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/canvas/canvas.js +0 -172
  166. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/canvas/dragging.js +0 -258
  167. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/canvas/resize.js +0 -48
  168. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/canvas/selection.js +0 -71
  169. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/core/api.js +0 -288
  170. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/core/state.js +0 -143
  171. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/core/utils.js +0 -245
  172. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/dev/element-inspector.js +0 -992
  173. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/editor/bbox.js +0 -339
  174. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/editor/element-drag.js +0 -286
  175. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/editor/overlay.js +0 -371
  176. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/editor/preview.js +0 -293
  177. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/main.js +0 -426
  178. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/shortcuts/context-menu.js +0 -152
  179. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/shortcuts/keyboard.js +0 -265
  180. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/ui/controls.js +0 -184
  181. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/ui/download.js +0 -57
  182. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/ui/help.js +0 -100
  183. scitex/fts/_fig/_editor/_gui/_flask_editor/static/js/ui/theme.js +0 -34
  184. scitex/fts/_fig/_editor/_gui/_flask_editor/templates/__init__.py +0 -124
  185. scitex/fts/_fig/_editor/_gui/_flask_editor/templates/_html.py +0 -851
  186. scitex/fts/_fig/_editor/_gui/_flask_editor/templates/_scripts.py +0 -4932
  187. scitex/fts/_fig/_editor/_gui/_flask_editor/templates/_styles.py +0 -1657
  188. scitex/fts/_fig/_editor/_gui/_flask_editor.py +0 -36
  189. scitex/fts/_fig/_models/_Annotations.py +0 -115
  190. scitex/fts/_fig/_models/_Axes.py +0 -152
  191. scitex/fts/_fig/_models/_Figure.py +0 -138
  192. scitex/fts/_fig/_models/_Plot.py +0 -123
  193. scitex/fts/_fig/_utils/_plot_layout.py +0 -397
  194. scitex/fts/_kinds/_figure/_composite.py +0 -345
  195. scitex/fts/_kinds/_plot/_backend/__init__.py +0 -53
  196. scitex/fts/_kinds/_plot/_backend/_export.py +0 -165
  197. scitex/fts/_kinds/_plot/_backend/_render.py +0 -538
  198. scitex/fts/_kinds/_plot/_dataclasses/_ChannelEncoding.py +0 -46
  199. scitex/fts/_kinds/_plot/_dataclasses/_Encoding.py +0 -82
  200. scitex/fts/_kinds/_plot/_dataclasses/_Theme.py +0 -441
  201. scitex/fts/_kinds/_plot/_dataclasses/_TraceEncoding.py +0 -52
  202. scitex/fts/_kinds/_plot/_dataclasses/__init__.py +0 -47
  203. scitex/fts/_kinds/_plot/_models/_Guides.py +0 -104
  204. scitex/fts/_kinds/_plot/_models/_Styles.py +0 -245
  205. scitex/fts/_kinds/_plot/_models/__init__.py +0 -80
  206. scitex/fts/_kinds/_plot/_models/_plot_types/__init__.py +0 -156
  207. scitex/fts/_kinds/_plot/_models/_plot_types/_bar.py +0 -43
  208. scitex/fts/_kinds/_plot/_models/_plot_types/_box.py +0 -38
  209. scitex/fts/_kinds/_plot/_models/_plot_types/_distribution.py +0 -36
  210. scitex/fts/_kinds/_plot/_models/_plot_types/_errorbar.py +0 -60
  211. scitex/fts/_kinds/_plot/_models/_plot_types/_histogram.py +0 -30
  212. scitex/fts/_kinds/_plot/_models/_plot_types/_image.py +0 -61
  213. scitex/fts/_kinds/_plot/_models/_plot_types/_line.py +0 -57
  214. scitex/fts/_kinds/_plot/_models/_plot_types/_scatter.py +0 -30
  215. scitex/fts/_kinds/_plot/_models/_plot_types/_seaborn.py +0 -121
  216. scitex/fts/_kinds/_plot/_models/_plot_types/_violin.py +0 -36
  217. scitex/fts/_kinds/_plot/_utils/__init__.py +0 -129
  218. scitex/fts/_kinds/_plot/_utils/_auto_layout.py +0 -127
  219. scitex/fts/_kinds/_plot/_utils/_calc_bounds.py +0 -111
  220. scitex/fts/_kinds/_plot/_utils/_const_sizes.py +0 -48
  221. scitex/fts/_kinds/_plot/_utils/_convert_coords.py +0 -77
  222. scitex/fts/_kinds/_plot/_utils/_get_template.py +0 -178
  223. scitex/fts/_kinds/_plot/_utils/_normalize.py +0 -73
  224. scitex/fts/_kinds/_plot/_utils/_validate.py +0 -197
  225. scitex/fts/_kinds/_table/_latex/_export.py +0 -279
  226. scitex/fts/_stats/__init__.py +0 -48
  227. scitex/fts/_stats/_dataclasses/_Stats.py +0 -423
  228. scitex/fts/_stats/_dataclasses/__init__.py +0 -48
  229. scitex/fts/_tables/__init__.py +0 -65
  230. scitex/fts/_tables/_latex/__init__.py +0 -93
  231. scitex/fts/_tables/_latex/_editor/__init__.py +0 -11
  232. scitex/fts/_tables/_latex/_editor/_app.py +0 -725
  233. scitex/fts/_tables/_latex/_figure_exporter.py +0 -153
  234. scitex/fts/_tables/_latex/_stats_formatter.py +0 -274
  235. scitex/fts/_tables/_latex/_table_exporter.py +0 -362
  236. scitex/fts/_tables/_latex/_utils.py +0 -369
  237. scitex/fts/_tables/_latex/_validator.py +0 -445
  238. scitex/io/_save_modules/_pltz_bundle.py +0 -356
  239. /scitex/{fig → canvas}/README.md +0 -0
  240. /scitex/{fig → canvas}/backend/__init__.py +0 -0
  241. /scitex/{fig → canvas}/backend/_export.py +0 -0
  242. /scitex/{fig → canvas}/backend/_render.py +0 -0
  243. /scitex/{fig → canvas}/docs/CANVAS_ARCHITECTURE.md +0 -0
  244. /scitex/{fig → canvas}/editor/__init__.py +0 -0
  245. /scitex/{fig → canvas}/editor/_dearpygui_editor.py +0 -0
  246. /scitex/{fig → canvas}/editor/_flask_editor.py +0 -0
  247. /scitex/{fig → canvas}/editor/_mpl_editor.py +0 -0
  248. /scitex/{fig → canvas}/editor/_qt_editor.py +0 -0
  249. /scitex/{fig → canvas}/editor/_tkinter_editor.py +0 -0
  250. /scitex/{fig → canvas}/editor/edit/backend_detector.py +0 -0
  251. /scitex/{fig → canvas}/editor/edit/manual_handler.py +0 -0
  252. /scitex/{fig → canvas}/editor/edit/path_resolver.py +0 -0
  253. /scitex/{fig → canvas}/editor/flask_editor/__init__.py +0 -0
  254. /scitex/{fig → canvas}/editor/flask_editor/_plotter.py +0 -0
  255. /scitex/{fig → canvas}/editor/flask_editor/_utils.py +0 -0
  256. /scitex/{fig → canvas}/editor/flask_editor/static/css/base/reset.css +0 -0
  257. /scitex/{fig → canvas}/editor/flask_editor/static/css/base/typography.css +0 -0
  258. /scitex/{fig → canvas}/editor/flask_editor/static/css/base/variables.css +0 -0
  259. /scitex/{fig → canvas}/editor/flask_editor/static/css/components/buttons.css +0 -0
  260. /scitex/{fig → canvas}/editor/flask_editor/static/css/components/context-menu.css +0 -0
  261. /scitex/{fig → canvas}/editor/flask_editor/static/css/components/dropdown.css +0 -0
  262. /scitex/{fig → canvas}/editor/flask_editor/static/css/components/forms.css +0 -0
  263. /scitex/{fig → canvas}/editor/flask_editor/static/css/components/modal.css +0 -0
  264. /scitex/{fig → canvas}/editor/flask_editor/static/css/components/sections.css +0 -0
  265. /scitex/{fig → canvas}/editor/flask_editor/static/css/features/element-inspector.css +0 -0
  266. /scitex/{fig → canvas}/editor/flask_editor/static/css/features/loading.css +0 -0
  267. /scitex/{fig → canvas}/editor/flask_editor/static/css/features/overlay.css +0 -0
  268. /scitex/{fig → canvas}/editor/flask_editor/static/css/features/selection.css +0 -0
  269. /scitex/{fig → canvas}/editor/flask_editor/static/css/features/statistics.css +0 -0
  270. /scitex/{fig → canvas}/editor/flask_editor/static/css/index.css +0 -0
  271. /scitex/{fig → canvas}/editor/flask_editor/static/css/layout/container.css +0 -0
  272. /scitex/{fig → canvas}/editor/flask_editor/static/css/layout/controls.css +0 -0
  273. /scitex/{fig → canvas}/editor/flask_editor/static/css/layout/preview.css +0 -0
  274. /scitex/{fig → canvas}/editor/flask_editor/static/js/alignment/axis.js +0 -0
  275. /scitex/{fig → canvas}/editor/flask_editor/static/js/alignment/basic.js +0 -0
  276. /scitex/{fig → canvas}/editor/flask_editor/static/js/alignment/distribute.js +0 -0
  277. /scitex/{fig → canvas}/editor/flask_editor/static/js/canvas/canvas.js +0 -0
  278. /scitex/{fig → canvas}/editor/flask_editor/static/js/canvas/dragging.js +0 -0
  279. /scitex/{fig → canvas}/editor/flask_editor/static/js/canvas/resize.js +0 -0
  280. /scitex/{fig → canvas}/editor/flask_editor/static/js/canvas/selection.js +0 -0
  281. /scitex/{fig → canvas}/editor/flask_editor/static/js/core/state.js +0 -0
  282. /scitex/{fig → canvas}/editor/flask_editor/static/js/core/utils.js +0 -0
  283. /scitex/{fig → canvas}/editor/flask_editor/static/js/dev/element-inspector.js +0 -0
  284. /scitex/{fig → canvas}/editor/flask_editor/static/js/editor/bbox.js +0 -0
  285. /scitex/{fig → canvas}/editor/flask_editor/static/js/editor/element-drag.js +0 -0
  286. /scitex/{fig → canvas}/editor/flask_editor/static/js/editor/overlay.js +0 -0
  287. /scitex/{fig → canvas}/editor/flask_editor/static/js/main.js +0 -0
  288. /scitex/{fig → canvas}/editor/flask_editor/static/js/shortcuts/context-menu.js +0 -0
  289. /scitex/{fig → canvas}/editor/flask_editor/static/js/shortcuts/keyboard.js +0 -0
  290. /scitex/{fig → canvas}/editor/flask_editor/static/js/ui/controls.js +0 -0
  291. /scitex/{fig → canvas}/editor/flask_editor/static/js/ui/download.js +0 -0
  292. /scitex/{fig → canvas}/editor/flask_editor/static/js/ui/help.js +0 -0
  293. /scitex/{fig → canvas}/editor/flask_editor/static/js/ui/theme.js +0 -0
  294. /scitex/{fig → canvas}/editor/flask_editor/templates/__init__.py +0 -0
  295. /scitex/{fig → canvas}/io/_directory.py +0 -0
  296. /scitex/{fig → canvas}/model/_plot_types.py +0 -0
  297. /scitex/{fig → canvas}/utils/_defaults.py +0 -0
  298. /scitex/{fig → canvas}/utils/_validate.py +0 -0
  299. /scitex/{fts/_bundle → io/bundle}/_conversion/__init__.py +0 -0
  300. /scitex/{fts/_bundle → io/bundle}/_conversion/_bundle2dict.py +0 -0
  301. /scitex/{fts/_bundle → io/bundle}/_conversion/_dict2bundle.py +0 -0
  302. /scitex/{fts/_bundle → io/bundle}/_dataclasses/_Axes.py +0 -0
  303. /scitex/{fts/_bundle → io/bundle}/_dataclasses/_BBox.py +0 -0
  304. /scitex/{fts/_bundle → io/bundle}/_dataclasses/_ColumnDef.py +0 -0
  305. /scitex/{fts/_bundle → io/bundle}/_dataclasses/_DataFormat.py +0 -0
  306. /scitex/{fts/_bundle → io/bundle}/_dataclasses/_DataInfo.py +0 -0
  307. /scitex/{fts/_bundle → io/bundle}/_dataclasses/_DataSource.py +0 -0
  308. /scitex/{fts/_bundle → io/bundle}/_dataclasses/_SizeMM.py +0 -0
  309. /scitex/{fts/_bundle → io/bundle}/_extractors/__init__.py +0 -0
  310. /scitex/{fts/_bundle → io/bundle}/_extractors/_extract_bar.py +0 -0
  311. /scitex/{fts/_bundle → io/bundle}/_extractors/_extract_line.py +0 -0
  312. /scitex/{fts/_bundle → io/bundle}/_extractors/_extract_scatter.py +0 -0
  313. /scitex/{fts/_kinds → io/bundle/kinds}/__init__.py +0 -0
  314. /scitex/{fts/_kinds → io/bundle/kinds}/_figure/__init__.py +0 -0
  315. /scitex/{fts/_fig → io/bundle/kinds/_figure}/_composite.py +0 -0
  316. /scitex/{fts/_kinds → io/bundle/kinds}/_plot/__init__.py +0 -0
  317. /scitex/{fts/_fig → io/bundle/kinds/_plot}/_backend/__init__.py +0 -0
  318. /scitex/{fts/_fig → io/bundle/kinds/_plot}/_backend/_export.py +0 -0
  319. /scitex/{fts/_fig → io/bundle/kinds/_plot}/_backend/_render.py +0 -0
  320. /scitex/{fts/_fig → io/bundle/kinds/_plot}/_dataclasses/_ChannelEncoding.py +0 -0
  321. /scitex/{fts/_fig → io/bundle/kinds/_plot}/_dataclasses/_Encoding.py +0 -0
  322. /scitex/{fts/_fig → io/bundle/kinds/_plot}/_dataclasses/_Theme.py +0 -0
  323. /scitex/{fts/_fig → io/bundle/kinds/_plot}/_dataclasses/_TraceEncoding.py +0 -0
  324. /scitex/{fts/_fig → io/bundle/kinds/_plot}/_dataclasses/__init__.py +0 -0
  325. /scitex/{fts/_fig → io/bundle/kinds/_plot}/_models/__init__.py +0 -0
  326. /scitex/{fts/_fig → io/bundle/kinds/_plot}/_models/_plot_types/__init__.py +0 -0
  327. /scitex/{fts/_fig → io/bundle/kinds/_plot}/_models/_plot_types/_bar.py +0 -0
  328. /scitex/{fts/_fig → io/bundle/kinds/_plot}/_models/_plot_types/_box.py +0 -0
  329. /scitex/{fts/_fig → io/bundle/kinds/_plot}/_models/_plot_types/_distribution.py +0 -0
  330. /scitex/{fts/_fig → io/bundle/kinds/_plot}/_models/_plot_types/_errorbar.py +0 -0
  331. /scitex/{fts/_fig → io/bundle/kinds/_plot}/_models/_plot_types/_histogram.py +0 -0
  332. /scitex/{fts/_fig → io/bundle/kinds/_plot}/_models/_plot_types/_image.py +0 -0
  333. /scitex/{fts/_fig → io/bundle/kinds/_plot}/_models/_plot_types/_line.py +0 -0
  334. /scitex/{fts/_fig → io/bundle/kinds/_plot}/_models/_plot_types/_scatter.py +0 -0
  335. /scitex/{fts/_fig → io/bundle/kinds/_plot}/_models/_plot_types/_seaborn.py +0 -0
  336. /scitex/{fts/_fig → io/bundle/kinds/_plot}/_models/_plot_types/_violin.py +0 -0
  337. /scitex/{fts/_fig → io/bundle/kinds/_plot}/_utils/__init__.py +0 -0
  338. /scitex/{fts/_fig → io/bundle/kinds/_plot}/_utils/_auto_layout.py +0 -0
  339. /scitex/{fts/_fig → io/bundle/kinds/_plot}/_utils/_calc_bounds.py +0 -0
  340. /scitex/{fts/_fig → io/bundle/kinds/_plot}/_utils/_const_sizes.py +0 -0
  341. /scitex/{fts/_fig → io/bundle/kinds/_plot}/_utils/_convert_coords.py +0 -0
  342. /scitex/{fts/_fig → io/bundle/kinds/_plot}/_utils/_get_template.py +0 -0
  343. /scitex/{fts/_fig → io/bundle/kinds/_plot}/_utils/_normalize.py +0 -0
  344. /scitex/{fts/_fig → io/bundle/kinds/_plot}/_utils/_validate.py +0 -0
  345. /scitex/{fts/_kinds → io/bundle/kinds}/_shape/__init__.py +0 -0
  346. /scitex/{fts/_kinds → io/bundle/kinds}/_stats/__init__.py +0 -0
  347. /scitex/{fts/_kinds → io/bundle/kinds}/_stats/_dataclasses/_Stats.py +0 -0
  348. /scitex/{fts/_kinds → io/bundle/kinds}/_stats/_dataclasses/__init__.py +0 -0
  349. /scitex/{fts/_kinds → io/bundle/kinds}/_table/__init__.py +0 -0
  350. /scitex/{fts/_kinds → io/bundle/kinds}/_table/_latex/_editor/__init__.py +0 -0
  351. /scitex/{fts/_kinds → io/bundle/kinds}/_table/_latex/_stats_formatter.py +0 -0
  352. /scitex/{fts/_kinds → io/bundle/kinds}/_table/_latex/_utils.py +0 -0
  353. /scitex/{fts/_kinds → io/bundle/kinds}/_table/_latex/_validator.py +0 -0
  354. /scitex/{fts/_kinds → io/bundle/kinds}/_text/__init__.py +0 -0
  355. /scitex/{fts/_schemas → io/bundle/schemas}/data_info.schema.json +0 -0
  356. /scitex/{fts/_schemas → io/bundle/schemas}/encoding.schema.json +0 -0
  357. /scitex/{fts/_schemas → io/bundle/schemas}/node.schema.json +0 -0
  358. /scitex/{fts/_schemas → io/bundle/schemas}/render_manifest.schema.json +0 -0
  359. /scitex/{fts/_schemas → io/bundle/schemas}/stats.schema.json +0 -0
  360. /scitex/{fts/_schemas → io/bundle/schemas}/theme.schema.json +0 -0
  361. {scitex-2.10.3.dist-info → scitex-2.11.0.dist-info}/WHEEL +0 -0
  362. {scitex-2.10.3.dist-info → scitex-2.11.0.dist-info}/entry_points.txt +0 -0
  363. {scitex-2.10.3.dist-info → scitex-2.11.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,1429 +0,0 @@
1
- #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
- # File: ./src/scitex/vis/editor/flask_editor/core.py
4
- """Core WebEditor class for Flask-based figure editing."""
5
-
6
- from pathlib import Path
7
- from typing import Dict, Any, Optional
8
- import base64
9
- import copy
10
- import json
11
- import threading
12
- import webbrowser
13
-
14
- from ._utils import find_available_port, kill_process_on_port, check_port_available
15
- from .templates import build_html_template
16
-
17
-
18
- class WebEditor:
19
- """
20
- Browser-based figure editor using Flask.
21
-
22
- Features:
23
- - Displays existing PNG from pltz bundle (no re-rendering)
24
- - Hitmap-based element selection for precise clicking
25
- - Property editors with sliders and color pickers
26
- - Save to .manual.json
27
- - SciTeX style defaults pre-filled
28
- - Auto-finds available port if default is in use
29
- """
30
-
31
- def __init__(
32
- self,
33
- json_path: Path,
34
- metadata: Dict[str, Any],
35
- csv_data: Optional[Any] = None,
36
- png_path: Optional[Path] = None,
37
- hitmap_path: Optional[Path] = None,
38
- manual_overrides: Optional[Dict[str, Any]] = None,
39
- port: int = 5050,
40
- panel_info: Optional[Dict[str, Any]] = None,
41
- ):
42
- self.json_path = Path(json_path)
43
- self.metadata = metadata
44
- self.csv_data = csv_data
45
- self.png_path = Path(png_path) if png_path else None
46
- self.hitmap_path = Path(hitmap_path) if hitmap_path else None
47
- self.manual_overrides = manual_overrides or {}
48
- self._requested_port = port
49
- self.port = port
50
- self.panel_info = panel_info # For multi-panel figz bundles
51
-
52
- # Extract hit_regions from metadata for color-based element detection
53
- self.hit_regions = metadata.get("hit_regions", {})
54
- self.color_map = self.hit_regions.get("color_map", {})
55
-
56
- # Get SciTeX defaults and merge with metadata
57
- from .._defaults import get_scitex_defaults, extract_defaults_from_metadata
58
-
59
- self.scitex_defaults = get_scitex_defaults()
60
- self.metadata_defaults = extract_defaults_from_metadata(metadata)
61
-
62
- # Start with defaults, then overlay manual overrides
63
- self.current_overrides = copy.deepcopy(self.scitex_defaults)
64
- self.current_overrides.update(self.metadata_defaults)
65
- self.current_overrides.update(self.manual_overrides)
66
-
67
- # Track initial state to detect modifications
68
- self._initial_overrides = copy.deepcopy(self.current_overrides)
69
- self._user_modified = False
70
-
71
- def run(self):
72
- """Launch the web editor."""
73
- try:
74
- from flask import Flask, render_template_string, request, jsonify
75
- except ImportError:
76
- raise ImportError(
77
- "Flask is required for web editor. Install: pip install flask"
78
- )
79
-
80
- # Handle port conflicts - always use port 5050
81
- import time
82
- max_retries = 3
83
- for attempt in range(max_retries):
84
- if check_port_available(self._requested_port):
85
- self.port = self._requested_port
86
- break
87
- print(f"Port {self._requested_port} in use. Freeing... (attempt {attempt + 1}/{max_retries})")
88
- kill_process_on_port(self._requested_port)
89
- time.sleep(1.0) # Wait for port release
90
- else:
91
- # After retries, use requested port anyway (Flask will error if unavailable)
92
- print(f"Warning: Port {self._requested_port} may still be in use")
93
- self.port = self._requested_port
94
-
95
- # Configure Flask with static folder path
96
- import os
97
- static_folder = os.path.join(os.path.dirname(__file__), 'static')
98
- app = Flask(__name__, static_folder=static_folder, static_url_path='/static')
99
- editor = self
100
-
101
- def _export_composed_figure(editor, formats=["png", "svg"], dpi=150):
102
- """Helper to compose and export figure to bundle."""
103
- from scitex.io import ZipBundle
104
- from PIL import Image
105
- import numpy as np
106
- import matplotlib
107
- matplotlib.use('Agg')
108
- import matplotlib.pyplot as plt
109
- import json as json_module
110
- import io
111
- import zipfile
112
-
113
- if not editor.panel_info:
114
- return {"success": False, "error": "No panel info"}
115
-
116
- bundle_path = editor.panel_info.get("bundle_path")
117
- figz_dir = editor.panel_info.get("figz_dir")
118
-
119
- if not bundle_path and not figz_dir:
120
- return {"success": False, "error": "No bundle path"}
121
-
122
- figure_name = Path(bundle_path).stem if bundle_path else (
123
- Path(figz_dir).stem.replace(".figz.d", "") if figz_dir else "figure"
124
- )
125
-
126
- # Read spec.json for layout and layout.json for position overrides
127
- spec = {}
128
- layout_overrides = {}
129
- if bundle_path:
130
- try:
131
- with ZipBundle(bundle_path, mode="r") as bundle:
132
- spec = bundle.read_json("spec.json")
133
- try:
134
- layout_overrides = bundle.read_json("layout.json")
135
- except:
136
- pass
137
- except:
138
- pass
139
- elif figz_dir:
140
- spec_path = Path(figz_dir) / "spec.json"
141
- if spec_path.exists():
142
- with open(spec_path) as f:
143
- spec = json_module.load(f)
144
- layout_path = Path(figz_dir) / "layout.json"
145
- if layout_path.exists():
146
- with open(layout_path) as f:
147
- layout_overrides = json_module.load(f)
148
-
149
- # Also check in-memory layout overrides
150
- if editor.panel_info and editor.panel_info.get("layout"):
151
- layout_overrides = editor.panel_info.get("layout", {})
152
-
153
- # Get figure dimensions
154
- fig_width_mm = 180
155
- fig_height_mm = 120
156
- if "figure" in spec:
157
- fig_info = spec.get("figure", {})
158
- styles = fig_info.get("styles", {})
159
- size = styles.get("size", {})
160
- fig_width_mm = size.get("width_mm", 180)
161
- fig_height_mm = size.get("height_mm", 120)
162
-
163
- fig_width_in = fig_width_mm / 25.4
164
- fig_height_in = fig_height_mm / 25.4
165
-
166
- fig = plt.figure(figsize=(fig_width_in, fig_height_in), dpi=dpi, facecolor='white')
167
-
168
- # Compose panels
169
- panels_spec = spec.get("panels", [])
170
- panel_paths = editor.panel_info.get("panel_paths", [])
171
- panel_is_zip = editor.panel_info.get("panel_is_zip", [])
172
-
173
- for panel_spec in panels_spec:
174
- panel_id = panel_spec.get("id", "")
175
- pos = panel_spec.get("position", {})
176
- size = panel_spec.get("size", {})
177
-
178
- # Skip overview/auxiliary panels (only compose main panels A-Z)
179
- panel_id_lower = panel_id.lower()
180
- if any(skip in panel_id_lower for skip in ['overview', 'thumb', 'preview', 'aux']):
181
- continue
182
-
183
- # Find panel path first (needed to check layout_overrides)
184
- panel_path = None
185
- is_zip = False
186
- panel_name = None
187
- for idx, pp in enumerate(panel_paths):
188
- pp_name = Path(pp).stem.replace(".pltz", "")
189
- if (pp_name == panel_id or
190
- pp_name.startswith(f"panel_{panel_id}_") or
191
- pp_name == f"panel_{panel_id}" or
192
- f"_{panel_id}_" in pp_name):
193
- panel_path = pp
194
- panel_name = Path(pp).name # e.g., "panel_A_twinx.pltz"
195
- is_zip = panel_is_zip[idx] if idx < len(panel_is_zip) else False
196
- break
197
-
198
- if not panel_path:
199
- continue
200
-
201
- # Check for layout overrides (from layout.json or in-memory)
202
- override = layout_overrides.get(panel_name, {})
203
- override_pos = override.get("position", {})
204
- override_size = override.get("size", {})
205
-
206
- # Use override positions if available, otherwise use spec
207
- x_mm = override_pos.get("x_mm", pos.get("x_mm", 0))
208
- y_mm = override_pos.get("y_mm", pos.get("y_mm", 0))
209
- w_mm = override_size.get("width_mm", size.get("width_mm", 60))
210
- h_mm = override_size.get("height_mm", size.get("height_mm", 40))
211
-
212
- x_frac = x_mm / fig_width_mm
213
- y_frac = 1 - (y_mm + h_mm) / fig_height_mm
214
- w_frac = w_mm / fig_width_mm
215
- h_frac = h_mm / fig_height_mm
216
-
217
- # Load panel preview
218
- try:
219
- # Exclusion patterns for preview selection
220
- exclude_patterns = ['hitmap', 'overview', 'thumb', 'preview']
221
-
222
- if is_zip:
223
- with ZipBundle(panel_path, mode="r") as pltz_bundle:
224
- with zipfile.ZipFile(panel_path, 'r') as zf:
225
- png_files = [n for n in zf.namelist()
226
- if n.endswith('.png')
227
- and 'exports/' in n
228
- and not any(p in n.lower() for p in exclude_patterns)]
229
- if png_files:
230
- preview_path = png_files[0]
231
- if '.pltz.d/' in preview_path:
232
- preview_path = preview_path.split('.pltz.d/')[-1]
233
- img_data = pltz_bundle.read_bytes(preview_path)
234
- img = Image.open(io.BytesIO(img_data))
235
- ax = fig.add_axes([x_frac, y_frac, w_frac, h_frac])
236
- ax.imshow(np.array(img))
237
- ax.axis('off')
238
- else:
239
- pltz_dir = Path(panel_path)
240
- exports_dir = pltz_dir / "exports"
241
- if exports_dir.exists():
242
- for png_file in exports_dir.glob("*.png"):
243
- name_lower = png_file.name.lower()
244
- if not any(p in name_lower for p in exclude_patterns):
245
- img = Image.open(png_file)
246
- ax = fig.add_axes([x_frac, y_frac, w_frac, h_frac])
247
- ax.imshow(np.array(img))
248
- ax.axis('off')
249
- break
250
- except Exception as e:
251
- print(f"Could not load panel {panel_id}: {e}")
252
-
253
- # Draw panel letter
254
- if panel_id and len(panel_id) <= 2: # Only for short IDs like A, B, C...
255
- # Position letter at top-left corner of panel
256
- letter_x = x_frac + 0.01
257
- letter_y = y_frac + h_frac - 0.02
258
- fig.text(letter_x, letter_y, panel_id,
259
- fontsize=14, fontweight='bold', color='black',
260
- ha='left', va='top',
261
- transform=fig.transFigure,
262
- bbox=dict(boxstyle='square,pad=0.1',
263
- facecolor='white', edgecolor='none', alpha=0.8))
264
-
265
- exported = {}
266
-
267
- # Save to bundle
268
- if bundle_path:
269
- with ZipBundle(bundle_path, mode="a") as bundle:
270
- for fmt in formats:
271
- buf = io.BytesIO()
272
- fig.savefig(buf, format=fmt, dpi=dpi, bbox_inches="tight",
273
- facecolor="white", pad_inches=0.02)
274
- buf.seek(0)
275
- export_path = f"exports/{figure_name}.{fmt}"
276
- bundle.write_bytes(export_path, buf.read())
277
- exported[fmt] = export_path
278
-
279
- plt.close(fig)
280
- return {"success": True, "exported": exported}
281
-
282
- @app.route("/")
283
- def index():
284
- # Rebuild template each time for hot reload support
285
- html_template = build_html_template()
286
-
287
- # Extract figz and panel paths for display
288
- json_path_str = str(editor.json_path.resolve())
289
- figz_path = ""
290
- panel_path = ""
291
-
292
- # Check if this is inside a figz bundle
293
- if '.figz.d/' in json_path_str:
294
- parts = json_path_str.split('.figz.d/')
295
- figz_path = parts[0] + '.figz.d'
296
- panel_path = parts[1] if len(parts) > 1 else ""
297
- elif '.pltz.d/' in json_path_str:
298
- parts = json_path_str.split('.pltz.d/')
299
- figz_path = parts[0] + '.pltz.d'
300
- panel_path = parts[1] if len(parts) > 1 else ""
301
- else:
302
- figz_path = json_path_str
303
-
304
- return render_template_string(
305
- html_template,
306
- filename=figz_path,
307
- panel_path=panel_path,
308
- overrides=json.dumps(editor.current_overrides),
309
- )
310
-
311
- @app.route("/preview")
312
- def preview():
313
- """Render figure preview with current overrides (same logic as /update)."""
314
- from ._renderer import render_preview_with_bboxes
315
-
316
- # Always use renderer for consistency between initial and updated views
317
- dark_mode = request.args.get("dark_mode", "false").lower() == "true"
318
- img_data, bboxes, img_size = render_preview_with_bboxes(
319
- editor.csv_data, editor.current_overrides,
320
- metadata=editor.metadata,
321
- dark_mode=dark_mode,
322
- )
323
- return jsonify({
324
- "image": img_data,
325
- "bboxes": bboxes,
326
- "img_size": img_size,
327
- "has_hitmap": editor.hitmap_path is not None and editor.hitmap_path.exists(),
328
- "format": "png",
329
- "panel_info": editor.panel_info,
330
- })
331
-
332
- @app.route("/panels")
333
- def panels():
334
- """Return all panel images with bboxes for interactive grid view (figz bundles only).
335
-
336
- Uses smart load_panel_data helper for transparent zip/directory handling.
337
- Returns layout info from figz spec.json for unified canvas positioning.
338
- """
339
- from ._bbox import extract_bboxes_from_metadata, extract_bboxes_from_geometry_px
340
- from ..edit import load_panel_data
341
- import json as json_module
342
-
343
- if not editor.panel_info:
344
- return jsonify({"error": "Not a multi-panel figz bundle"}), 400
345
-
346
- panel_names = editor.panel_info["panels"]
347
- panel_paths = editor.panel_info.get("panel_paths", [])
348
- panel_is_zip = editor.panel_info.get("panel_is_zip", [False] * len(panel_names))
349
- figz_dir = Path(editor.panel_info["figz_dir"])
350
-
351
- if not panel_paths:
352
- panel_paths = [str(figz_dir / name) for name in panel_names]
353
-
354
- # Load figz spec.json to get panel layout
355
- figz_layout = {}
356
- spec_path = figz_dir / "spec.json"
357
- if spec_path.exists():
358
- with open(spec_path) as f:
359
- figz_spec = json_module.load(f)
360
- for panel_spec in figz_spec.get("panels", []):
361
- panel_id = panel_spec.get("id", "")
362
- figz_layout[panel_id] = {
363
- "position": panel_spec.get("position", {}),
364
- "size": panel_spec.get("size", {}),
365
- }
366
-
367
- panel_images = []
368
-
369
- for idx, panel_name in enumerate(panel_names):
370
- panel_path = panel_paths[idx]
371
- is_zip = panel_is_zip[idx] if idx < len(panel_is_zip) else None
372
- display_name = panel_name.replace(".pltz.d", "").replace(".pltz", "")
373
-
374
- # Use smart helper to load panel data
375
- loaded = load_panel_data(panel_path, is_zip=is_zip)
376
-
377
- panel_data = {"name": display_name, "image": None, "bboxes": None, "img_size": None}
378
-
379
- # Add layout info from figz spec
380
- if display_name in figz_layout:
381
- panel_data["layout"] = figz_layout[display_name]
382
-
383
- if loaded:
384
- # Get image data
385
- if loaded.get("is_zip"):
386
- png_bytes = loaded.get("png_bytes")
387
- if png_bytes:
388
- panel_data["image"] = base64.b64encode(png_bytes).decode("utf-8")
389
- else:
390
- png_path = loaded.get("png_path")
391
- if png_path and png_path.exists():
392
- with open(png_path, "rb") as f:
393
- panel_data["image"] = base64.b64encode(f.read()).decode("utf-8")
394
-
395
- # Get image size
396
- img_size = loaded.get("img_size")
397
- if img_size:
398
- panel_data["img_size"] = img_size
399
- panel_data["width"] = img_size["width"]
400
- panel_data["height"] = img_size["height"]
401
- elif loaded.get("png_path"):
402
- from PIL import Image
403
- img = Image.open(loaded["png_path"])
404
- panel_data["img_size"] = {"width": img.size[0], "height": img.size[1]}
405
- panel_data["width"], panel_data["height"] = img.size
406
- img.close()
407
-
408
- # Extract bboxes - prefer geometry_px.json
409
- if panel_data.get("img_size"):
410
- geometry_data = loaded.get("geometry_data")
411
- metadata = loaded.get("metadata", {})
412
-
413
- if geometry_data:
414
- panel_data["bboxes"] = extract_bboxes_from_geometry_px(
415
- geometry_data,
416
- panel_data["img_size"]["width"],
417
- panel_data["img_size"]["height"]
418
- )
419
- elif metadata:
420
- panel_data["bboxes"] = extract_bboxes_from_metadata(
421
- metadata,
422
- panel_data["img_size"]["width"],
423
- panel_data["img_size"]["height"]
424
- )
425
-
426
- panel_images.append(panel_data)
427
-
428
- return jsonify({
429
- "panels": panel_images,
430
- "count": len(panel_images),
431
- "layout": figz_layout,
432
- })
433
-
434
- @app.route("/switch_panel/<int:panel_index>")
435
- def switch_panel(panel_index):
436
- """Switch to a different panel in the figz bundle.
437
-
438
- Uses smart load_panel_data helper for transparent zip/directory handling.
439
- """
440
- from ._bbox import extract_bboxes_from_metadata, extract_bboxes_from_geometry_px
441
- from ..edit import load_panel_data
442
-
443
- if not editor.panel_info:
444
- return jsonify({"error": "Not a multi-panel figz bundle"}), 400
445
-
446
- panels = editor.panel_info["panels"]
447
- panel_paths = editor.panel_info.get("panel_paths", [])
448
- panel_is_zip = editor.panel_info.get("panel_is_zip", [False] * len(panels))
449
-
450
- if panel_index < 0 or panel_index >= len(panels):
451
- return jsonify({"error": f"Invalid panel index: {panel_index}"}), 400
452
-
453
- panel_name = panels[panel_index]
454
- panel_path = panel_paths[panel_index] if panel_paths else str(Path(editor.panel_info["figz_dir"]) / panel_name)
455
- is_zip = panel_is_zip[panel_index] if panel_index < len(panel_is_zip) else None
456
-
457
- try:
458
- # Use smart helper to load panel data
459
- loaded = load_panel_data(panel_path, is_zip=is_zip)
460
-
461
- if not loaded:
462
- return jsonify({"error": f"Could not load panel: {panel_name}"}), 400
463
-
464
- # Get image data
465
- img_data = None
466
- if loaded.get("is_zip"):
467
- png_bytes = loaded.get("png_bytes")
468
- if png_bytes:
469
- img_data = base64.b64encode(png_bytes).decode("utf-8")
470
- else:
471
- png_path = loaded.get("png_path")
472
- if png_path and png_path.exists():
473
- with open(png_path, "rb") as f:
474
- img_data = base64.b64encode(f.read()).decode("utf-8")
475
-
476
- if not img_data:
477
- return jsonify({"error": f"No PNG found for panel: {panel_name}"}), 400
478
-
479
- # Get image size
480
- img_size = loaded.get("img_size", {"width": 0, "height": 0})
481
- if not img_size and loaded.get("png_path"):
482
- from PIL import Image
483
- img = Image.open(loaded["png_path"])
484
- img_size = {"width": img.size[0], "height": img.size[1]}
485
- img.close()
486
-
487
- # Extract bboxes - prefer geometry_px.json
488
- bboxes = {}
489
- geometry_data = loaded.get("geometry_data")
490
- metadata = loaded.get("metadata", {})
491
-
492
- if geometry_data and img_size:
493
- bboxes = extract_bboxes_from_geometry_px(
494
- geometry_data,
495
- img_size["width"],
496
- img_size["height"]
497
- )
498
- elif metadata and img_size:
499
- bboxes = extract_bboxes_from_metadata(
500
- metadata,
501
- img_size["width"],
502
- img_size["height"]
503
- )
504
-
505
- # Update editor state
506
- editor.metadata = metadata
507
- editor.panel_info["current_index"] = panel_index
508
-
509
- # Re-extract defaults from new metadata
510
- from .._defaults import get_scitex_defaults, extract_defaults_from_metadata
511
- editor.scitex_defaults = get_scitex_defaults()
512
- editor.metadata_defaults = extract_defaults_from_metadata(metadata)
513
- editor.current_overrides = copy.deepcopy(editor.scitex_defaults)
514
- editor.current_overrides.update(editor.metadata_defaults)
515
- editor.current_overrides.update(editor.manual_overrides)
516
-
517
- return jsonify({
518
- "success": True,
519
- "panel_name": panel_name,
520
- "panel_index": panel_index,
521
- "image": img_data,
522
- "bboxes": bboxes,
523
- "img_size": img_size,
524
- "overrides": editor.current_overrides,
525
- })
526
- except Exception as e:
527
- import traceback
528
- return jsonify({
529
- "error": f"Failed to switch panel: {str(e)}",
530
- "traceback": traceback.format_exc(),
531
- }), 500
532
-
533
- @app.route("/hitmap")
534
- def hitmap():
535
- """Return hitmap PNG for element detection."""
536
- if editor.hitmap_path and editor.hitmap_path.exists():
537
- with open(editor.hitmap_path, "rb") as f:
538
- img_data = base64.b64encode(f.read()).decode("utf-8")
539
- return jsonify({
540
- "image": img_data,
541
- "color_map": editor.color_map,
542
- })
543
- return jsonify({"error": "No hitmap available"}), 404
544
-
545
- @app.route("/color_map")
546
- def color_map():
547
- """Return color map for hitmap element identification."""
548
- return jsonify({
549
- "color_map": editor.color_map,
550
- "hit_regions": editor.hit_regions,
551
- })
552
-
553
- @app.route("/update", methods=["POST"])
554
- def update():
555
- """Update overrides and re-render with updated properties."""
556
- from ._renderer import render_preview_with_bboxes
557
-
558
- data = request.json
559
- editor.current_overrides.update(data.get("overrides", {}))
560
- editor._user_modified = True
561
-
562
- # Check if dark mode is requested from POST data
563
- dark_mode = data.get("dark_mode", False)
564
-
565
- # Re-render the figure with updated overrides
566
- img_data, bboxes, img_size = render_preview_with_bboxes(
567
- editor.csv_data, editor.current_overrides,
568
- metadata=editor.metadata,
569
- dark_mode=dark_mode,
570
- )
571
- return jsonify({
572
- "image": img_data,
573
- "bboxes": bboxes,
574
- "img_size": img_size,
575
- "status": "updated",
576
- })
577
-
578
- @app.route("/save", methods=["POST"])
579
- def save():
580
- """Save to .manual.json."""
581
- from ..edit import save_manual_overrides
582
-
583
- try:
584
- manual_path = save_manual_overrides(
585
- editor.json_path, editor.current_overrides
586
- )
587
- return jsonify({"status": "saved", "path": str(manual_path)})
588
- except Exception as e:
589
- return jsonify({"status": "error", "message": str(e)}), 500
590
-
591
- @app.route("/save_layout", methods=["POST"])
592
- def save_layout():
593
- """Save panel layout positions to figz bundle."""
594
- try:
595
- data = request.get_json()
596
- layout = data.get("layout", {})
597
-
598
- if not layout:
599
- return jsonify({"success": False, "error": "No layout data provided"})
600
-
601
- # Check if we have panel_info (figz bundle)
602
- if not editor.panel_info:
603
- return jsonify({"success": False, "error": "No panel info available (not a figz bundle)"})
604
-
605
- bundle_path = editor.panel_info.get("bundle_path")
606
- if not bundle_path:
607
- return jsonify({"success": False, "error": "Bundle path not available"})
608
-
609
- # Update layout in the figz bundle
610
- from scitex.fig.io import ZipBundle
611
-
612
- bundle = ZipBundle(bundle_path)
613
-
614
- # Read existing layout or create new one
615
- try:
616
- existing_layout = bundle.read_json("layout.json")
617
- except:
618
- existing_layout = {}
619
-
620
- # Update layout with new positions
621
- for panel_name, pos in layout.items():
622
- if panel_name not in existing_layout:
623
- existing_layout[panel_name] = {}
624
- if "position" not in existing_layout[panel_name]:
625
- existing_layout[panel_name]["position"] = {}
626
- if "size" not in existing_layout[panel_name]:
627
- existing_layout[panel_name]["size"] = {}
628
-
629
- # Update position
630
- existing_layout[panel_name]["position"]["x_mm"] = pos.get("x_mm", 0)
631
- existing_layout[panel_name]["position"]["y_mm"] = pos.get("y_mm", 0)
632
-
633
- # Update size if provided
634
- if "width_mm" in pos:
635
- existing_layout[panel_name]["size"]["width_mm"] = pos["width_mm"]
636
- if "height_mm" in pos:
637
- existing_layout[panel_name]["size"]["height_mm"] = pos["height_mm"]
638
-
639
- # Save updated layout
640
- bundle.write_json("layout.json", existing_layout)
641
-
642
- # Update in-memory panel_info
643
- editor.panel_info["layout"] = existing_layout
644
-
645
- # Auto-export composed figure to bundle
646
- export_result = _export_composed_figure(editor, formats=["png", "svg"])
647
-
648
- return jsonify({
649
- "success": True,
650
- "layout": existing_layout,
651
- "exported": export_result.get("exported", {})
652
- })
653
-
654
- except Exception as e:
655
- import traceback
656
- return jsonify({
657
- "success": False,
658
- "error": str(e),
659
- "traceback": traceback.format_exc()
660
- })
661
-
662
- @app.route("/save_element_position", methods=["POST"])
663
- def save_element_position():
664
- """Save element position (legend/panel_letter) to figz bundle.
665
-
666
- ONLY legends and panel letters can be repositioned to maintain
667
- scientific rigor. Data elements are never moved.
668
- """
669
- try:
670
- data = request.get_json()
671
- element = data.get("element", "")
672
- panel = data.get("panel", "")
673
- element_type = data.get("element_type", "")
674
- position = data.get("position", {})
675
- snap_name = data.get("snap_name")
676
-
677
- # Validate element type (whitelist for scientific rigor)
678
- ALLOWED_TYPES = ["legend", "panel_letter"]
679
- if element_type not in ALLOWED_TYPES:
680
- return jsonify({
681
- "success": False,
682
- "error": f"Element type '{element_type}' cannot be repositioned (scientific rigor)"
683
- })
684
-
685
- if not editor.panel_info:
686
- return jsonify({"success": False, "error": "No panel info available"})
687
-
688
- bundle_path = editor.panel_info.get("bundle_path")
689
- if not bundle_path:
690
- return jsonify({"success": False, "error": "Bundle path not available"})
691
-
692
- from scitex.fig.io import ZipBundle
693
- bundle = ZipBundle(bundle_path)
694
-
695
- # Read or create style.json for element positions
696
- try:
697
- style = bundle.read_json("style.json")
698
- except:
699
- style = {}
700
-
701
- # Initialize structure
702
- if "elements" not in style:
703
- style["elements"] = {}
704
- if panel not in style["elements"]:
705
- style["elements"][panel] = {}
706
-
707
- # Save element position
708
- style["elements"][panel][element] = {
709
- "type": element_type,
710
- "position": position,
711
- "snap_name": snap_name,
712
- }
713
-
714
- # For legends, also update legend_location for matplotlib compatibility
715
- if element_type == "legend" and snap_name:
716
- # Convert snap name to matplotlib loc format
717
- loc_map = {
718
- "upper left": "upper left",
719
- "upper center": "upper center",
720
- "upper right": "upper right",
721
- "center left": "center left",
722
- "center": "center",
723
- "center right": "center right",
724
- "lower left": "lower left",
725
- "lower center": "lower center",
726
- "lower right": "lower right",
727
- }
728
- if snap_name in loc_map:
729
- if "legend" not in style:
730
- style["legend"] = {}
731
- style["legend"]["location"] = loc_map[snap_name]
732
-
733
- bundle.write_json("style.json", style)
734
-
735
- return jsonify({
736
- "success": True,
737
- "element": element,
738
- "position": position,
739
- "snap_name": snap_name
740
- })
741
-
742
- except Exception as e:
743
- import traceback
744
- return jsonify({
745
- "success": False,
746
- "error": str(e),
747
- "traceback": traceback.format_exc()
748
- })
749
-
750
- @app.route("/export", methods=["POST"])
751
- def export_figure():
752
- """Export composed figure to various formats and update figz bundle."""
753
- try:
754
- data = request.get_json()
755
- formats = data.get("formats", ["png", "svg"])
756
-
757
- if not editor.panel_info:
758
- return jsonify({"success": False, "error": "No panel info available"})
759
-
760
- bundle_path = editor.panel_info.get("bundle_path")
761
- if not bundle_path:
762
- return jsonify({"success": False, "error": "Bundle path not available"})
763
-
764
- from scitex.io import ZipBundle
765
- from pathlib import Path
766
- import io
767
- import matplotlib
768
- matplotlib.use('Agg')
769
- import matplotlib.pyplot as plt
770
- from PIL import Image
771
- import numpy as np
772
-
773
- figure_name = Path(bundle_path).stem
774
- dpi = data.get("dpi", 150)
775
-
776
- with ZipBundle(bundle_path, mode="a") as bundle:
777
- # Read spec for figure size and panel positions
778
- try:
779
- spec = bundle.read_json("spec.json")
780
- except:
781
- spec = {}
782
-
783
- # Get figure dimensions
784
- fig_width_mm = 180
785
- fig_height_mm = 120
786
- if "figure" in spec:
787
- fig_info = spec.get("figure", {})
788
- styles = fig_info.get("styles", {})
789
- size = styles.get("size", {})
790
- fig_width_mm = size.get("width_mm", 180)
791
- fig_height_mm = size.get("height_mm", 120)
792
-
793
- # Convert mm to inches
794
- fig_width_in = fig_width_mm / 25.4
795
- fig_height_in = fig_height_mm / 25.4
796
-
797
- # Create figure with white background
798
- fig = plt.figure(figsize=(fig_width_in, fig_height_in), dpi=dpi, facecolor='white')
799
-
800
- # Get panels from spec or editor.panel_info
801
- panels_spec = spec.get("panels", [])
802
-
803
- # Compose panels onto figure
804
- for panel_spec in panels_spec:
805
- panel_id = panel_spec.get("id", "")
806
- pltz_name = panel_spec.get("plot", "")
807
-
808
- # Get position and size from spec
809
- pos = panel_spec.get("position", {})
810
- size = panel_spec.get("size", {})
811
-
812
- x_mm = pos.get("x_mm", 0)
813
- y_mm = pos.get("y_mm", 0)
814
- w_mm = size.get("width_mm", 60)
815
- h_mm = size.get("height_mm", 40)
816
-
817
- # Convert to figure coordinates (0-1)
818
- x_frac = x_mm / fig_width_mm
819
- y_frac = 1 - (y_mm + h_mm) / fig_height_mm # Flip Y
820
- w_frac = w_mm / fig_width_mm
821
- h_frac = h_mm / fig_height_mm
822
-
823
- # Try to read panel image from pltz exports
824
- img_loaded = False
825
- for pltz_path in [f"{panel_id}.pltz", pltz_name.replace(".d", "")]:
826
- if img_loaded:
827
- break
828
- try:
829
- # Read pltz as nested bundle
830
- pltz_bytes = bundle.read_bytes(pltz_path)
831
- import tempfile
832
- with tempfile.NamedTemporaryFile(suffix=".pltz", delete=False) as tmp:
833
- tmp.write(pltz_bytes)
834
- tmp_path = tmp.name
835
- try:
836
- with ZipBundle(tmp_path, mode="r") as pltz_bundle:
837
- # Try various preview paths
838
- for preview_path in ["exports/preview.png", "preview.png", f"exports/{panel_id}.png"]:
839
- try:
840
- img_data = pltz_bundle.read_bytes(preview_path)
841
- img = Image.open(io.BytesIO(img_data))
842
- img_array = np.array(img)
843
-
844
- # Create axes and add image
845
- ax = fig.add_axes([x_frac, y_frac, w_frac, h_frac])
846
- ax.imshow(img_array)
847
- ax.axis('off')
848
- img_loaded = True
849
- break
850
- except:
851
- continue
852
- finally:
853
- import os
854
- os.unlink(tmp_path)
855
- except Exception as e:
856
- print(f"Could not load pltz {pltz_path}: {e}")
857
- continue
858
-
859
- exported = {}
860
-
861
- for fmt in formats:
862
- buf = io.BytesIO()
863
- if fmt in ["png", "jpeg", "jpg"]:
864
- fig.savefig(buf, format="png" if fmt == "png" else "jpeg",
865
- dpi=dpi, bbox_inches="tight", facecolor="white",
866
- pad_inches=0.02)
867
- elif fmt == "svg":
868
- fig.savefig(buf, format="svg", bbox_inches="tight", pad_inches=0.02)
869
- elif fmt == "pdf":
870
- fig.savefig(buf, format="pdf", bbox_inches="tight", pad_inches=0.02)
871
- else:
872
- continue
873
-
874
- buf.seek(0)
875
- content = buf.read()
876
-
877
- # Save to exports/ directory in bundle
878
- export_path = f"exports/{figure_name}.{fmt}"
879
- bundle.write_bytes(export_path, content)
880
- exported[fmt] = export_path
881
-
882
- plt.close(fig)
883
-
884
- return jsonify({
885
- "success": True,
886
- "exported": exported,
887
- "bundle_path": str(bundle_path)
888
- })
889
-
890
- except Exception as e:
891
- import traceback
892
- return jsonify({
893
- "success": False,
894
- "error": str(e),
895
- "traceback": traceback.format_exc()
896
- })
897
-
898
- @app.route("/download/<fmt>")
899
- def download_figure(fmt):
900
- """Download figure in specified format."""
901
- try:
902
- from flask import send_file
903
- import io
904
- from pathlib import Path
905
-
906
- mime_types = {
907
- "png": "image/png",
908
- "jpeg": "image/jpeg",
909
- "jpg": "image/jpeg",
910
- "svg": "image/svg+xml",
911
- "pdf": "application/pdf",
912
- }
913
-
914
- if fmt not in mime_types:
915
- return f"Unsupported format: {fmt}", 400
916
-
917
- # For figz bundles, download the composed figure
918
- if editor.panel_info:
919
- bundle_path = editor.panel_info.get("bundle_path")
920
- figz_dir = editor.panel_info.get("figz_dir")
921
- figure_name = Path(bundle_path).stem if bundle_path else (
922
- Path(figz_dir).stem.replace(".figz.d", "") if figz_dir else "figure"
923
- )
924
-
925
- if bundle_path or figz_dir:
926
- from scitex.io import ZipBundle
927
- from PIL import Image
928
- import numpy as np
929
- import matplotlib
930
- matplotlib.use('Agg')
931
- import matplotlib.pyplot as plt
932
- import json as json_module
933
-
934
- # Always compose on-demand to ensure current panel state
935
- # (existing exports in bundle may be stale or blank)
936
-
937
- # Read spec.json and layout.json for position overrides
938
- spec = {}
939
- layout_overrides = {}
940
- if bundle_path:
941
- try:
942
- with ZipBundle(bundle_path, mode="r") as bundle:
943
- spec = bundle.read_json("spec.json")
944
- try:
945
- layout_overrides = bundle.read_json("layout.json")
946
- except:
947
- pass
948
- except:
949
- pass
950
- elif figz_dir:
951
- spec_path = Path(figz_dir) / "spec.json"
952
- if spec_path.exists():
953
- with open(spec_path) as f:
954
- spec = json_module.load(f)
955
- layout_path = Path(figz_dir) / "layout.json"
956
- if layout_path.exists():
957
- with open(layout_path) as f:
958
- layout_overrides = json_module.load(f)
959
-
960
- # Also check in-memory layout overrides (most current)
961
- if editor.panel_info and editor.panel_info.get("layout"):
962
- layout_overrides = editor.panel_info.get("layout", {})
963
-
964
- # Get figure dimensions
965
- fig_width_mm = 180
966
- fig_height_mm = 120
967
- if "figure" in spec:
968
- fig_info = spec.get("figure", {})
969
- styles = fig_info.get("styles", {})
970
- size = styles.get("size", {})
971
- fig_width_mm = size.get("width_mm", 180)
972
- fig_height_mm = size.get("height_mm", 120)
973
-
974
- fig_width_in = fig_width_mm / 25.4
975
- fig_height_in = fig_height_mm / 25.4
976
-
977
- dpi = 150 if fmt in ["jpeg", "jpg"] else 300
978
- fig = plt.figure(figsize=(fig_width_in, fig_height_in), dpi=dpi, facecolor='white')
979
-
980
- # Compose panels
981
- panels_spec = spec.get("panels", [])
982
- panel_paths = editor.panel_info.get("panel_paths", [])
983
- panel_is_zip = editor.panel_info.get("panel_is_zip", [])
984
-
985
- for panel_spec in panels_spec:
986
- panel_id = panel_spec.get("id", "")
987
- pos = panel_spec.get("position", {})
988
- size = panel_spec.get("size", {})
989
-
990
- # Skip overview/auxiliary panels (only compose main panels A-Z)
991
- panel_id_lower = panel_id.lower()
992
- if any(skip in panel_id_lower for skip in ['overview', 'thumb', 'preview', 'aux']):
993
- continue
994
-
995
- # Find panel path first (needed to check layout_overrides)
996
- panel_path = None
997
- is_zip = False
998
- panel_name = None
999
- for idx, pp in enumerate(panel_paths):
1000
- pp_name = Path(pp).stem.replace(".pltz", "")
1001
- # Match exact name, or name contains panel_id pattern
1002
- # e.g., "panel_A_twinx" matches panel_id "A"
1003
- if (pp_name == panel_id or
1004
- pp_name.startswith(f"panel_{panel_id}_") or
1005
- pp_name.startswith(f"panel_{panel_id}.") or
1006
- pp_name == f"panel_{panel_id}" or
1007
- pp_name == panel_id or
1008
- f"_{panel_id}_" in pp_name or
1009
- pp_name.endswith(f"_{panel_id}")):
1010
- panel_path = pp
1011
- panel_name = Path(pp).name # e.g., "panel_A_twinx.pltz"
1012
- is_zip = panel_is_zip[idx] if idx < len(panel_is_zip) else False
1013
- break
1014
-
1015
- if not panel_path:
1016
- print(f"Could not find panel path for id={panel_id}, available: {[Path(p).stem for p in panel_paths]}")
1017
- continue
1018
-
1019
- # Check for layout overrides (from layout.json or in-memory)
1020
- override = layout_overrides.get(panel_name, {})
1021
- override_pos = override.get("position", {})
1022
- override_size = override.get("size", {})
1023
-
1024
- # Use override positions if available, otherwise use spec
1025
- x_mm = override_pos.get("x_mm", pos.get("x_mm", 0))
1026
- y_mm = override_pos.get("y_mm", pos.get("y_mm", 0))
1027
- w_mm = override_size.get("width_mm", size.get("width_mm", 60))
1028
- h_mm = override_size.get("height_mm", size.get("height_mm", 40))
1029
-
1030
- x_frac = x_mm / fig_width_mm
1031
- y_frac = 1 - (y_mm + h_mm) / fig_height_mm
1032
- w_frac = w_mm / fig_width_mm
1033
- h_frac = h_mm / fig_height_mm
1034
-
1035
- # Load panel preview image
1036
- try:
1037
- img_loaded = False
1038
- # Exclusion patterns for preview selection
1039
- exclude_patterns = ['hitmap', 'overview', 'thumb', 'preview']
1040
-
1041
- if is_zip:
1042
- with ZipBundle(panel_path, mode="r") as pltz_bundle:
1043
- # Find PNG in exports (exclude hitmap, overview, thumbnails)
1044
- import zipfile
1045
- with zipfile.ZipFile(panel_path, 'r') as zf:
1046
- png_files = [n for n in zf.namelist()
1047
- if n.endswith('.png')
1048
- and 'exports/' in n
1049
- and not any(p in n.lower() for p in exclude_patterns)]
1050
- if png_files:
1051
- # Use first matching PNG
1052
- preview_path = png_files[0]
1053
- # Extract the path relative to .d directory
1054
- if '.pltz.d/' in preview_path:
1055
- preview_path = preview_path.split('.pltz.d/')[-1]
1056
- try:
1057
- img_data = pltz_bundle.read_bytes(preview_path)
1058
- img = Image.open(io.BytesIO(img_data))
1059
- ax = fig.add_axes([x_frac, y_frac, w_frac, h_frac])
1060
- ax.imshow(np.array(img))
1061
- ax.axis('off')
1062
- img_loaded = True
1063
- except Exception as e:
1064
- print(f"Could not read {preview_path}: {e}")
1065
- else:
1066
- # Directory-based pltz
1067
- pltz_dir = Path(panel_path)
1068
- exports_dir = pltz_dir / "exports"
1069
- if exports_dir.exists():
1070
- for png_file in exports_dir.glob("*.png"):
1071
- name_lower = png_file.name.lower()
1072
- if not any(p in name_lower for p in exclude_patterns):
1073
- img = Image.open(png_file)
1074
- ax = fig.add_axes([x_frac, y_frac, w_frac, h_frac])
1075
- ax.imshow(np.array(img))
1076
- ax.axis('off')
1077
- img_loaded = True
1078
- break
1079
- if not img_loaded:
1080
- print(f"No preview found for panel {panel_id}")
1081
- except Exception as e:
1082
- print(f"Could not load panel {panel_id}: {e}")
1083
-
1084
- # Draw panel letter
1085
- if panel_id and len(panel_id) <= 2: # Only for short IDs like A, B, C...
1086
- # Position letter at top-left corner of panel
1087
- letter_x = x_frac + 0.01
1088
- letter_y = y_frac + h_frac - 0.02
1089
- fig.text(letter_x, letter_y, panel_id,
1090
- fontsize=14, fontweight='bold', color='black',
1091
- ha='left', va='top',
1092
- transform=fig.transFigure,
1093
- bbox=dict(boxstyle='square,pad=0.1',
1094
- facecolor='white', edgecolor='none', alpha=0.8))
1095
-
1096
- buf = io.BytesIO()
1097
- fig.savefig(buf, format=fmt if fmt != "jpg" else "jpeg",
1098
- dpi=dpi, bbox_inches="tight", facecolor="white",
1099
- pad_inches=0.02)
1100
- plt.close(fig)
1101
- buf.seek(0)
1102
-
1103
- return send_file(
1104
- buf,
1105
- mimetype=mime_types[fmt],
1106
- as_attachment=True,
1107
- download_name=f"{figure_name}.{fmt}"
1108
- )
1109
-
1110
- # For single pltz files, render from csv_data
1111
- from ._renderer import render_preview_with_bboxes
1112
- import matplotlib
1113
- matplotlib.use('Agg')
1114
- import matplotlib.pyplot as plt
1115
-
1116
- figure_name = "figure"
1117
- if editor.json_path:
1118
- figure_name = Path(editor.json_path).stem
1119
-
1120
- img_data, _, _ = render_preview_with_bboxes(
1121
- editor.csv_data, editor.current_overrides,
1122
- metadata=editor.metadata,
1123
- dark_mode=False,
1124
- )
1125
-
1126
- if fmt == "png":
1127
- import base64
1128
- content = base64.b64decode(img_data)
1129
- buf = io.BytesIO(content)
1130
- return send_file(
1131
- buf,
1132
- mimetype=mime_types[fmt],
1133
- as_attachment=True,
1134
- download_name=f"{figure_name}.{fmt}"
1135
- )
1136
-
1137
- # For other formats, re-render
1138
- from ._plotter import plot_from_csv
1139
- fig, ax = plt.subplots(figsize=(8, 6))
1140
- plot_from_csv(ax, editor.csv_data, editor.current_overrides)
1141
-
1142
- buf = io.BytesIO()
1143
- dpi = 150 if fmt in ["jpeg", "jpg"] else 300
1144
- fig.savefig(buf, format=fmt if fmt != "jpg" else "jpeg",
1145
- dpi=dpi, bbox_inches="tight",
1146
- facecolor="white" if fmt in ["jpeg", "jpg"] else None)
1147
- plt.close(fig)
1148
- buf.seek(0)
1149
-
1150
- return send_file(
1151
- buf,
1152
- mimetype=mime_types[fmt],
1153
- as_attachment=True,
1154
- download_name=f"{figure_name}.{fmt}"
1155
- )
1156
-
1157
- except Exception as e:
1158
- import traceback
1159
- return f"Error: {str(e)}\n{traceback.format_exc()}", 500
1160
-
1161
- @app.route("/download_figz")
1162
- def download_figz():
1163
- """Download as figz bundle (re-editable format)."""
1164
- try:
1165
- if not editor.panel_info:
1166
- return "No panel info available", 404
1167
-
1168
- bundle_path = editor.panel_info.get("bundle_path")
1169
- if not bundle_path:
1170
- return "Bundle path not available", 404
1171
-
1172
- from flask import send_file
1173
- from pathlib import Path
1174
-
1175
- # Send the figz file directly (it's already a pltz-compatible format)
1176
- return send_file(
1177
- bundle_path,
1178
- mimetype="application/zip",
1179
- as_attachment=True,
1180
- download_name=Path(bundle_path).name
1181
- )
1182
-
1183
- except Exception as e:
1184
- return str(e), 500
1185
-
1186
- @app.route("/shutdown", methods=["POST"])
1187
- def shutdown():
1188
- """Shutdown the server."""
1189
- func = request.environ.get("werkzeug.server.shutdown")
1190
- if func is None:
1191
- raise RuntimeError("Not running with Werkzeug Server")
1192
- func()
1193
- return jsonify({"status": "shutdown"})
1194
-
1195
- @app.route("/stats")
1196
- def stats():
1197
- """Return statistical test results from figure metadata."""
1198
- stats_data = editor.metadata.get("stats", [])
1199
- stats_summary = editor.metadata.get("stats_summary", None)
1200
- return jsonify({
1201
- "stats": stats_data,
1202
- "stats_summary": stats_summary,
1203
- "has_stats": len(stats_data) > 0,
1204
- })
1205
-
1206
- # Open browser after short delay
1207
- def open_browser():
1208
- import time
1209
-
1210
- time.sleep(0.5)
1211
- webbrowser.open(f"http://127.0.0.1:{self.port}")
1212
-
1213
- threading.Thread(target=open_browser, daemon=True).start()
1214
-
1215
- print(f"Starting SciTeX Figure Editor at http://127.0.0.1:{self.port}")
1216
- print("Press Ctrl+C to stop")
1217
-
1218
- # Note: use_reloader=False because the reloader re-runs the entire script
1219
- # which causes infinite loops when the demo generates figures
1220
- # Templates are rebuilt on each page refresh anyway
1221
- app.run(host="127.0.0.1", port=self.port, debug=False, use_reloader=False)
1222
-
1223
-
1224
- def _extract_bboxes_from_metadata(
1225
- metadata: Dict[str, Any],
1226
- display_width: Optional[float] = None,
1227
- display_height: Optional[float] = None
1228
- ) -> Dict[str, Any]:
1229
- """Extract element bounding boxes from pltz metadata.
1230
-
1231
- Builds bboxes from selectable_regions in the metadata for click detection.
1232
- This allows the editor to highlight elements when clicked.
1233
-
1234
- Coordinate system (new layered format):
1235
- - selectable_regions bbox_px: Already in final image space (figure_px)
1236
- - Display size: Actual displayed image size (PNG pixels or SVG viewBox)
1237
- - Scale = display_size / figure_px (usually 1:1, but may differ for scaled display)
1238
-
1239
- Parameters
1240
- ----------
1241
- metadata : dict
1242
- The pltz JSON metadata containing selectable_regions
1243
- display_width : float, optional
1244
- Actual display image width (from PNG size or SVG viewBox)
1245
- display_height : float, optional
1246
- Actual display image height (from PNG size or SVG viewBox)
1247
-
1248
- Returns
1249
- -------
1250
- dict
1251
- Mapping of element IDs to their bounding box coordinates (in display pixels)
1252
- """
1253
- bboxes = {}
1254
- selectable = metadata.get("selectable_regions", {})
1255
-
1256
- # Figure dimensions from new layered format (bbox_px are in this space)
1257
- figure_px = metadata.get("figure_px", [])
1258
- if isinstance(figure_px, list) and len(figure_px) >= 2:
1259
- fig_width = figure_px[0]
1260
- fig_height = figure_px[1]
1261
- else:
1262
- # Fallback for old format: try hit_regions.path_data.figure
1263
- hit_regions = metadata.get("hit_regions", {})
1264
- path_data = hit_regions.get("path_data", {})
1265
- orig_fig = path_data.get("figure", {})
1266
- fig_width = orig_fig.get("width_px", 944)
1267
- fig_height = orig_fig.get("height_px", 803)
1268
-
1269
- # Use actual display dimensions if provided, else use figure_px
1270
- if display_width is None:
1271
- display_width = fig_width
1272
- if display_height is None:
1273
- display_height = fig_height
1274
-
1275
- # Scale factor: display / figure_px
1276
- # Usually 1:1 since display is the same PNG, but may differ for scaled display
1277
- scale_x = display_width / fig_width if fig_width > 0 else 1
1278
- scale_y = display_height / fig_height if fig_height > 0 else 1
1279
-
1280
- # Helper to convert coords to display pixels
1281
- def to_display_bbox(bbox, is_list=True):
1282
- """Convert bbox to display pixels (apply scaling if display != figure_px).
1283
-
1284
- Parameters
1285
- ----------
1286
- bbox : list or dict
1287
- Bbox coordinates [x0, y0, x1, y1] or dict with keys
1288
- is_list : bool
1289
- Whether bbox is a list (True) or dict (False)
1290
- """
1291
- if is_list:
1292
- x0, y0, x1, y1 = bbox[0], bbox[1], bbox[2], bbox[3]
1293
- else:
1294
- x0 = bbox.get("x0", 0)
1295
- y0 = bbox.get("y0", 0)
1296
- x1 = bbox.get("x1", bbox.get("x0", 0) + bbox.get("width", 0))
1297
- y1 = bbox.get("y1", bbox.get("y0", 0) + bbox.get("height", 0))
1298
-
1299
- # Scale to display coords (usually 1:1)
1300
- disp_x0 = x0 * scale_x
1301
- disp_x1 = x1 * scale_x
1302
- disp_y0 = y0 * scale_y
1303
- disp_y1 = y1 * scale_y
1304
-
1305
- return {
1306
- "x0": disp_x0,
1307
- "y0": disp_y0,
1308
- "x1": disp_x1,
1309
- "y1": disp_y1,
1310
- "x": disp_x0,
1311
- "y": disp_y0,
1312
- "width": disp_x1 - disp_x0,
1313
- "height": disp_y1 - disp_y0,
1314
- }
1315
-
1316
- # Extract from selectable_regions.axes
1317
- axes_regions = selectable.get("axes", [])
1318
- for ax_idx, ax in enumerate(axes_regions):
1319
- ax_key = f"ax_{ax_idx:02d}"
1320
-
1321
- # Title
1322
- title = ax.get("title", {})
1323
- if title and "bbox_px" in title:
1324
- bbox_disp = to_display_bbox(title["bbox_px"])
1325
- bboxes[f"{ax_key}_title"] = {
1326
- **bbox_disp,
1327
- "type": "title",
1328
- "text": title.get("text", ""),
1329
- }
1330
-
1331
- # X label
1332
- xlabel = ax.get("xlabel", {})
1333
- if xlabel and "bbox_px" in xlabel:
1334
- bbox_disp = to_display_bbox(xlabel["bbox_px"])
1335
- bboxes[f"{ax_key}_xlabel"] = {
1336
- **bbox_disp,
1337
- "type": "xlabel",
1338
- "text": xlabel.get("text", ""),
1339
- }
1340
-
1341
- # Y label
1342
- ylabel = ax.get("ylabel", {})
1343
- if ylabel and "bbox_px" in ylabel:
1344
- bbox_disp = to_display_bbox(ylabel["bbox_px"])
1345
- bboxes[f"{ax_key}_ylabel"] = {
1346
- **bbox_disp,
1347
- "type": "ylabel",
1348
- "text": ylabel.get("text", ""),
1349
- }
1350
-
1351
- # Legend
1352
- legend = ax.get("legend", {})
1353
- if legend and "bbox_px" in legend:
1354
- bbox_disp = to_display_bbox(legend["bbox_px"])
1355
- bboxes[f"{ax_key}_legend"] = {
1356
- **bbox_disp,
1357
- "type": "legend",
1358
- }
1359
-
1360
- # X-axis spine
1361
- xaxis = ax.get("xaxis", {})
1362
- if xaxis:
1363
- spine = xaxis.get("spine", {})
1364
- if spine and "bbox_px" in spine:
1365
- bbox_disp = to_display_bbox(spine["bbox_px"])
1366
- bboxes[f"{ax_key}_xaxis_spine"] = {
1367
- **bbox_disp,
1368
- "type": "xaxis",
1369
- }
1370
-
1371
- # Y-axis spine
1372
- yaxis = ax.get("yaxis", {})
1373
- if yaxis:
1374
- spine = yaxis.get("spine", {})
1375
- if spine and "bbox_px" in spine:
1376
- bbox_disp = to_display_bbox(spine["bbox_px"])
1377
- bboxes[f"{ax_key}_yaxis_spine"] = {
1378
- **bbox_disp,
1379
- "type": "yaxis",
1380
- }
1381
-
1382
- # Extract traces from artists (top-level in new format, or hit_regions.path_data in old)
1383
- artists = metadata.get("artists", [])
1384
- if not artists:
1385
- # Fallback for old format
1386
- hit_regions = metadata.get("hit_regions", {})
1387
- path_data = hit_regions.get("path_data", {})
1388
- artists = path_data.get("artists", [])
1389
-
1390
- for artist in artists:
1391
- artist_id = artist.get("id", 0)
1392
- artist_type = artist.get("type", "line")
1393
- bbox_px = artist.get("bbox_px", {})
1394
- if bbox_px:
1395
- bbox_disp = to_display_bbox(bbox_px, is_list=False)
1396
- trace_entry = {
1397
- **bbox_disp,
1398
- "type": artist_type,
1399
- "label": artist.get("label", f"Trace {artist_id}"),
1400
- "element_type": artist_type,
1401
- }
1402
-
1403
- # Include scaled path points for line proximity detection
1404
- path_px = artist.get("path_px", [])
1405
- if path_px:
1406
- scaled_points = [
1407
- [pt[0] * scale_x, pt[1] * scale_y]
1408
- for pt in path_px if len(pt) >= 2
1409
- ]
1410
- trace_entry["points"] = scaled_points
1411
-
1412
- bboxes[f"trace_{artist_id}"] = trace_entry
1413
-
1414
- # Add metadata for JavaScript to understand the coordinate system
1415
- bboxes["_meta"] = {
1416
- "display_width": display_width,
1417
- "display_height": display_height,
1418
- "figure_px_width": fig_width,
1419
- "figure_px_height": fig_height,
1420
- "scale_x": scale_x,
1421
- "scale_y": scale_y,
1422
- # Note: With new layered format, bbox_px are already in final image space
1423
- # so scale is typically 1:1 (unless display is resized)
1424
- }
1425
-
1426
- return bboxes
1427
-
1428
-
1429
- # EOF