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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (297) hide show
  1. scitex/__init__.py +6 -2
  2. scitex/__version__.py +1 -1
  3. scitex/audio/README.md +52 -0
  4. scitex/audio/__init__.py +384 -0
  5. scitex/audio/__main__.py +129 -0
  6. scitex/audio/_tts.py +334 -0
  7. scitex/audio/engines/__init__.py +44 -0
  8. scitex/audio/engines/base.py +275 -0
  9. scitex/audio/engines/elevenlabs_engine.py +143 -0
  10. scitex/audio/engines/gtts_engine.py +162 -0
  11. scitex/audio/engines/pyttsx3_engine.py +131 -0
  12. scitex/audio/mcp_server.py +757 -0
  13. scitex/bridge/_helpers.py +1 -1
  14. scitex/bridge/_plt_vis.py +1 -1
  15. scitex/bridge/_stats_vis.py +1 -1
  16. scitex/dev/plt/__init__.py +272 -0
  17. scitex/dev/plt/plot_mpl_axhline.py +28 -0
  18. scitex/dev/plt/plot_mpl_axhspan.py +28 -0
  19. scitex/dev/plt/plot_mpl_axvline.py +28 -0
  20. scitex/dev/plt/plot_mpl_axvspan.py +28 -0
  21. scitex/dev/plt/plot_mpl_bar.py +29 -0
  22. scitex/dev/plt/plot_mpl_barh.py +29 -0
  23. scitex/dev/plt/plot_mpl_boxplot.py +28 -0
  24. scitex/dev/plt/plot_mpl_contour.py +31 -0
  25. scitex/dev/plt/plot_mpl_contourf.py +31 -0
  26. scitex/dev/plt/plot_mpl_errorbar.py +30 -0
  27. scitex/dev/plt/plot_mpl_eventplot.py +28 -0
  28. scitex/dev/plt/plot_mpl_fill.py +30 -0
  29. scitex/dev/plt/plot_mpl_fill_between.py +31 -0
  30. scitex/dev/plt/plot_mpl_hexbin.py +28 -0
  31. scitex/dev/plt/plot_mpl_hist.py +28 -0
  32. scitex/dev/plt/plot_mpl_hist2d.py +28 -0
  33. scitex/dev/plt/plot_mpl_imshow.py +29 -0
  34. scitex/dev/plt/plot_mpl_pcolormesh.py +31 -0
  35. scitex/dev/plt/plot_mpl_pie.py +29 -0
  36. scitex/dev/plt/plot_mpl_plot.py +29 -0
  37. scitex/dev/plt/plot_mpl_quiver.py +31 -0
  38. scitex/dev/plt/plot_mpl_scatter.py +28 -0
  39. scitex/dev/plt/plot_mpl_stackplot.py +31 -0
  40. scitex/dev/plt/plot_mpl_stem.py +29 -0
  41. scitex/dev/plt/plot_mpl_step.py +29 -0
  42. scitex/dev/plt/plot_mpl_violinplot.py +28 -0
  43. scitex/dev/plt/plot_sns_barplot.py +29 -0
  44. scitex/dev/plt/plot_sns_boxplot.py +29 -0
  45. scitex/dev/plt/plot_sns_heatmap.py +28 -0
  46. scitex/dev/plt/plot_sns_histplot.py +29 -0
  47. scitex/dev/plt/plot_sns_kdeplot.py +29 -0
  48. scitex/dev/plt/plot_sns_lineplot.py +31 -0
  49. scitex/dev/plt/plot_sns_scatterplot.py +29 -0
  50. scitex/dev/plt/plot_sns_stripplot.py +29 -0
  51. scitex/dev/plt/plot_sns_swarmplot.py +29 -0
  52. scitex/dev/plt/plot_sns_violinplot.py +29 -0
  53. scitex/dev/plt/plot_stx_bar.py +29 -0
  54. scitex/dev/plt/plot_stx_barh.py +29 -0
  55. scitex/dev/plt/plot_stx_box.py +28 -0
  56. scitex/dev/plt/plot_stx_boxplot.py +28 -0
  57. scitex/dev/plt/plot_stx_conf_mat.py +28 -0
  58. scitex/dev/plt/plot_stx_contour.py +31 -0
  59. scitex/dev/plt/plot_stx_ecdf.py +28 -0
  60. scitex/dev/plt/plot_stx_errorbar.py +30 -0
  61. scitex/dev/plt/plot_stx_fill_between.py +31 -0
  62. scitex/dev/plt/plot_stx_fillv.py +28 -0
  63. scitex/dev/plt/plot_stx_heatmap.py +28 -0
  64. scitex/dev/plt/plot_stx_image.py +28 -0
  65. scitex/dev/plt/plot_stx_imshow.py +28 -0
  66. scitex/dev/plt/plot_stx_joyplot.py +28 -0
  67. scitex/dev/plt/plot_stx_kde.py +28 -0
  68. scitex/dev/plt/plot_stx_line.py +28 -0
  69. scitex/dev/plt/plot_stx_mean_ci.py +28 -0
  70. scitex/dev/plt/plot_stx_mean_std.py +28 -0
  71. scitex/dev/plt/plot_stx_median_iqr.py +28 -0
  72. scitex/dev/plt/plot_stx_raster.py +28 -0
  73. scitex/dev/plt/plot_stx_rectangle.py +28 -0
  74. scitex/dev/plt/plot_stx_scatter.py +29 -0
  75. scitex/dev/plt/plot_stx_shaded_line.py +29 -0
  76. scitex/dev/plt/plot_stx_violin.py +28 -0
  77. scitex/dev/plt/plot_stx_violinplot.py +28 -0
  78. scitex/fig/__init__.py +352 -0
  79. scitex/{vis → fig}/backend/_parser.py +1 -1
  80. scitex/{vis → fig}/canvas.py +1 -1
  81. scitex/{vis → fig}/editor/_defaults.py +70 -5
  82. scitex/fig/editor/_edit.py +751 -0
  83. scitex/{vis → fig}/editor/_qt_editor.py +181 -1
  84. scitex/fig/editor/flask_editor/_bbox.py +1276 -0
  85. scitex/fig/editor/flask_editor/_core.py +624 -0
  86. scitex/{vis → fig}/editor/flask_editor/_plotter.py +38 -4
  87. scitex/fig/editor/flask_editor/_renderer.py +739 -0
  88. scitex/{vis → fig}/editor/flask_editor/templates/__init__.py +1 -1
  89. scitex/fig/editor/flask_editor/templates/_html.py +834 -0
  90. scitex/fig/editor/flask_editor/templates/_scripts.py +3136 -0
  91. scitex/{vis → fig}/editor/flask_editor/templates/_styles.py +625 -18
  92. scitex/{vis → fig}/io/__init__.py +13 -1
  93. scitex/fig/io/_bundle.py +973 -0
  94. scitex/{vis → fig}/io/_canvas.py +1 -1
  95. scitex/{vis → fig}/io/_data.py +1 -1
  96. scitex/{vis → fig}/io/_export.py +1 -1
  97. scitex/{vis → fig}/io/_load.py +1 -1
  98. scitex/{vis → fig}/io/_panel.py +1 -1
  99. scitex/{vis → fig}/io/_save.py +1 -1
  100. scitex/{vis → fig}/model/__init__.py +1 -1
  101. scitex/{vis → fig}/model/_annotations.py +1 -1
  102. scitex/{vis → fig}/model/_axes.py +1 -1
  103. scitex/{vis → fig}/model/_figure.py +1 -1
  104. scitex/{vis → fig}/model/_guides.py +1 -1
  105. scitex/{vis → fig}/model/_plot.py +1 -1
  106. scitex/{vis → fig}/model/_styles.py +1 -1
  107. scitex/{vis → fig}/utils/__init__.py +1 -1
  108. scitex/io/__init__.py +10 -26
  109. scitex/io/_bundle.py +434 -0
  110. scitex/io/_flush.py +5 -2
  111. scitex/io/_load.py +98 -0
  112. scitex/io/_load_modules/_H5Explorer.py +5 -2
  113. scitex/io/_load_modules/_canvas.py +2 -2
  114. scitex/io/_load_modules/_image.py +3 -4
  115. scitex/io/_load_modules/_txt.py +4 -2
  116. scitex/io/_metadata.py +34 -324
  117. scitex/io/_metadata_modules/__init__.py +46 -0
  118. scitex/io/_metadata_modules/_embed.py +70 -0
  119. scitex/io/_metadata_modules/_read.py +64 -0
  120. scitex/io/_metadata_modules/_utils.py +79 -0
  121. scitex/io/_metadata_modules/embed_metadata_jpeg.py +74 -0
  122. scitex/io/_metadata_modules/embed_metadata_pdf.py +53 -0
  123. scitex/io/_metadata_modules/embed_metadata_png.py +26 -0
  124. scitex/io/_metadata_modules/embed_metadata_svg.py +62 -0
  125. scitex/io/_metadata_modules/read_metadata_jpeg.py +57 -0
  126. scitex/io/_metadata_modules/read_metadata_pdf.py +51 -0
  127. scitex/io/_metadata_modules/read_metadata_png.py +39 -0
  128. scitex/io/_metadata_modules/read_metadata_svg.py +44 -0
  129. scitex/io/_qr_utils.py +5 -3
  130. scitex/io/_save.py +548 -30
  131. scitex/io/_save_modules/_canvas.py +3 -3
  132. scitex/io/_save_modules/_image.py +5 -9
  133. scitex/io/_save_modules/_tex.py +7 -4
  134. scitex/io/utils/h5_to_zarr.py +11 -9
  135. scitex/msword/__init__.py +255 -0
  136. scitex/msword/profiles.py +357 -0
  137. scitex/msword/reader.py +753 -0
  138. scitex/msword/utils.py +289 -0
  139. scitex/msword/writer.py +362 -0
  140. scitex/plt/__init__.py +5 -2
  141. scitex/plt/_subplots/_AxesWrapper.py +6 -6
  142. scitex/plt/_subplots/_AxisWrapper.py +15 -9
  143. scitex/plt/_subplots/_AxisWrapperMixins/_AdjustmentMixin/__init__.py +36 -0
  144. scitex/plt/_subplots/_AxisWrapperMixins/_AdjustmentMixin/_labels.py +264 -0
  145. scitex/plt/_subplots/_AxisWrapperMixins/_AdjustmentMixin/_metadata.py +213 -0
  146. scitex/plt/_subplots/_AxisWrapperMixins/_AdjustmentMixin/_visual.py +128 -0
  147. scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin/__init__.py +59 -0
  148. scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin/_base.py +34 -0
  149. scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin/_scientific.py +593 -0
  150. scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin/_statistical.py +654 -0
  151. scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin/_stx_aliases.py +527 -0
  152. scitex/plt/_subplots/_AxisWrapperMixins/_RawMatplotlibMixin.py +321 -0
  153. scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin/__init__.py +33 -0
  154. scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin/_base.py +152 -0
  155. scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin/_wrappers.py +600 -0
  156. scitex/plt/_subplots/_AxisWrapperMixins/__init__.py +79 -5
  157. scitex/plt/_subplots/_FigWrapper.py +6 -6
  158. scitex/plt/_subplots/_SubplotsWrapper.py +28 -18
  159. scitex/plt/_subplots/_export_as_csv.py +35 -5
  160. scitex/plt/_subplots/_export_as_csv_formatters/__init__.py +8 -0
  161. scitex/plt/_subplots/_export_as_csv_formatters/_format_annotate.py +10 -21
  162. scitex/plt/_subplots/_export_as_csv_formatters/_format_eventplot.py +18 -7
  163. scitex/plt/_subplots/_export_as_csv_formatters/_format_imshow2d.py +28 -12
  164. scitex/plt/_subplots/_export_as_csv_formatters/_format_matshow.py +10 -4
  165. scitex/plt/_subplots/_export_as_csv_formatters/_format_plot_imshow.py +13 -1
  166. scitex/plt/_subplots/_export_as_csv_formatters/_format_plot_kde.py +12 -2
  167. scitex/plt/_subplots/_export_as_csv_formatters/_format_plot_scatter.py +10 -3
  168. scitex/plt/_subplots/_export_as_csv_formatters/_format_quiver.py +10 -4
  169. scitex/plt/_subplots/_export_as_csv_formatters/_format_sns_jointplot.py +18 -3
  170. scitex/plt/_subplots/_export_as_csv_formatters/_format_sns_lineplot.py +44 -36
  171. scitex/plt/_subplots/_export_as_csv_formatters/_format_sns_pairplot.py +14 -2
  172. scitex/plt/_subplots/_export_as_csv_formatters/_format_streamplot.py +11 -5
  173. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_bar.py +84 -0
  174. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_barh.py +85 -0
  175. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_conf_mat.py +14 -3
  176. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_contour.py +54 -0
  177. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_ecdf.py +14 -2
  178. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_errorbar.py +120 -0
  179. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_heatmap.py +16 -6
  180. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_image.py +29 -19
  181. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_imshow.py +63 -0
  182. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_joyplot.py +22 -5
  183. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_mean_ci.py +18 -14
  184. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_mean_std.py +18 -14
  185. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_median_iqr.py +18 -14
  186. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_raster.py +10 -2
  187. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_scatter.py +51 -0
  188. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_scatter_hist.py +18 -9
  189. scitex/plt/ax/_plot/_stx_ecdf.py +4 -2
  190. scitex/plt/gallery/_generate.py +421 -14
  191. scitex/plt/io/__init__.py +53 -0
  192. scitex/plt/io/_bundle.py +490 -0
  193. scitex/plt/io/_layered_bundle.py +1343 -0
  194. scitex/plt/styles/SCITEX_STYLE.yaml +26 -0
  195. scitex/plt/styles/__init__.py +14 -0
  196. scitex/plt/styles/presets.py +78 -0
  197. scitex/plt/utils/__init__.py +13 -1
  198. scitex/plt/utils/_collect_figure_metadata.py +10 -14
  199. scitex/plt/utils/_configure_mpl.py +6 -18
  200. scitex/plt/utils/_crop.py +32 -14
  201. scitex/plt/utils/_csv_column_naming.py +54 -0
  202. scitex/plt/utils/_figure_mm.py +116 -1
  203. scitex/plt/utils/_hitmap.py +1643 -0
  204. scitex/plt/utils/metadata/__init__.py +25 -0
  205. scitex/plt/utils/metadata/_core.py +9 -10
  206. scitex/plt/utils/metadata/_dimensions.py +6 -3
  207. scitex/plt/utils/metadata/_editable_export.py +405 -0
  208. scitex/plt/utils/metadata/_geometry_extraction.py +570 -0
  209. scitex/schema/__init__.py +109 -16
  210. scitex/schema/_canvas.py +1 -1
  211. scitex/schema/_plot.py +1015 -0
  212. scitex/schema/_stats.py +2 -2
  213. scitex/stats/__init__.py +117 -0
  214. scitex/stats/io/__init__.py +29 -0
  215. scitex/stats/io/_bundle.py +156 -0
  216. scitex/tex/__init__.py +4 -0
  217. scitex/tex/_export.py +890 -0
  218. {scitex-2.7.0.dist-info → scitex-2.7.3.dist-info}/METADATA +11 -1
  219. {scitex-2.7.0.dist-info → scitex-2.7.3.dist-info}/RECORD +238 -170
  220. scitex/io/memo.md +0 -2827
  221. scitex/plt/REQUESTS.md +0 -191
  222. scitex/plt/_subplots/TODO.md +0 -53
  223. scitex/plt/_subplots/_AxisWrapperMixins/_AdjustmentMixin.py +0 -559
  224. scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin.py +0 -1609
  225. scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin.py +0 -447
  226. scitex/plt/templates/research-master/scitex/vis/gallery/area/fill_between.json +0 -110
  227. scitex/plt/templates/research-master/scitex/vis/gallery/area/fill_betweenx.json +0 -88
  228. scitex/plt/templates/research-master/scitex/vis/gallery/area/stx_fill_between.json +0 -103
  229. scitex/plt/templates/research-master/scitex/vis/gallery/area/stx_fillv.json +0 -106
  230. scitex/plt/templates/research-master/scitex/vis/gallery/categorical/bar.json +0 -92
  231. scitex/plt/templates/research-master/scitex/vis/gallery/categorical/barh.json +0 -92
  232. scitex/plt/templates/research-master/scitex/vis/gallery/categorical/boxplot.json +0 -92
  233. scitex/plt/templates/research-master/scitex/vis/gallery/categorical/stx_bar.json +0 -84
  234. scitex/plt/templates/research-master/scitex/vis/gallery/categorical/stx_barh.json +0 -84
  235. scitex/plt/templates/research-master/scitex/vis/gallery/categorical/stx_box.json +0 -83
  236. scitex/plt/templates/research-master/scitex/vis/gallery/categorical/stx_boxplot.json +0 -93
  237. scitex/plt/templates/research-master/scitex/vis/gallery/categorical/stx_violin.json +0 -91
  238. scitex/plt/templates/research-master/scitex/vis/gallery/categorical/stx_violinplot.json +0 -91
  239. scitex/plt/templates/research-master/scitex/vis/gallery/categorical/violinplot.json +0 -91
  240. scitex/plt/templates/research-master/scitex/vis/gallery/contour/contour.json +0 -97
  241. scitex/plt/templates/research-master/scitex/vis/gallery/contour/contourf.json +0 -98
  242. scitex/plt/templates/research-master/scitex/vis/gallery/contour/stx_contour.json +0 -84
  243. scitex/plt/templates/research-master/scitex/vis/gallery/distribution/hist.json +0 -101
  244. scitex/plt/templates/research-master/scitex/vis/gallery/distribution/hist2d.json +0 -96
  245. scitex/plt/templates/research-master/scitex/vis/gallery/distribution/stx_ecdf.json +0 -95
  246. scitex/plt/templates/research-master/scitex/vis/gallery/distribution/stx_joyplot.json +0 -95
  247. scitex/plt/templates/research-master/scitex/vis/gallery/distribution/stx_kde.json +0 -93
  248. scitex/plt/templates/research-master/scitex/vis/gallery/grid/imshow.json +0 -95
  249. scitex/plt/templates/research-master/scitex/vis/gallery/grid/matshow.json +0 -95
  250. scitex/plt/templates/research-master/scitex/vis/gallery/grid/stx_conf_mat.json +0 -83
  251. scitex/plt/templates/research-master/scitex/vis/gallery/grid/stx_heatmap.json +0 -92
  252. scitex/plt/templates/research-master/scitex/vis/gallery/grid/stx_image.json +0 -121
  253. scitex/plt/templates/research-master/scitex/vis/gallery/grid/stx_imshow.json +0 -84
  254. scitex/plt/templates/research-master/scitex/vis/gallery/line/plot.json +0 -110
  255. scitex/plt/templates/research-master/scitex/vis/gallery/line/step.json +0 -92
  256. scitex/plt/templates/research-master/scitex/vis/gallery/line/stx_line.json +0 -95
  257. scitex/plt/templates/research-master/scitex/vis/gallery/line/stx_shaded_line.json +0 -96
  258. scitex/plt/templates/research-master/scitex/vis/gallery/scatter/hexbin.json +0 -95
  259. scitex/plt/templates/research-master/scitex/vis/gallery/scatter/scatter.json +0 -95
  260. scitex/plt/templates/research-master/scitex/vis/gallery/scatter/stem.json +0 -92
  261. scitex/plt/templates/research-master/scitex/vis/gallery/scatter/stx_scatter.json +0 -84
  262. scitex/plt/templates/research-master/scitex/vis/gallery/special/pie.json +0 -94
  263. scitex/plt/templates/research-master/scitex/vis/gallery/special/stx_raster.json +0 -109
  264. scitex/plt/templates/research-master/scitex/vis/gallery/special/stx_rectangle.json +0 -108
  265. scitex/plt/templates/research-master/scitex/vis/gallery/statistical/errorbar.json +0 -93
  266. scitex/plt/templates/research-master/scitex/vis/gallery/statistical/stx_errorbar.json +0 -84
  267. scitex/plt/templates/research-master/scitex/vis/gallery/statistical/stx_mean_ci.json +0 -96
  268. scitex/plt/templates/research-master/scitex/vis/gallery/statistical/stx_mean_std.json +0 -96
  269. scitex/plt/templates/research-master/scitex/vis/gallery/statistical/stx_median_iqr.json +0 -96
  270. scitex/plt/templates/research-master/scitex/vis/gallery/vector/quiver.json +0 -99
  271. scitex/plt/templates/research-master/scitex/vis/gallery/vector/streamplot.json +0 -100
  272. scitex/vis/__init__.py +0 -177
  273. scitex/vis/editor/_edit.py +0 -390
  274. scitex/vis/editor/flask_editor/_bbox.py +0 -529
  275. scitex/vis/editor/flask_editor/_core.py +0 -168
  276. scitex/vis/editor/flask_editor/_renderer.py +0 -393
  277. scitex/vis/editor/flask_editor/templates/_html.py +0 -513
  278. scitex/vis/editor/flask_editor/templates/_scripts.py +0 -1261
  279. /scitex/{vis → fig}/README.md +0 -0
  280. /scitex/{vis → fig}/backend/__init__.py +0 -0
  281. /scitex/{vis → fig}/backend/_export.py +0 -0
  282. /scitex/{vis → fig}/backend/_render.py +0 -0
  283. /scitex/{vis → fig}/docs/CANVAS_ARCHITECTURE.md +0 -0
  284. /scitex/{vis → fig}/editor/__init__.py +0 -0
  285. /scitex/{vis → fig}/editor/_dearpygui_editor.py +0 -0
  286. /scitex/{vis → fig}/editor/_flask_editor.py +0 -0
  287. /scitex/{vis → fig}/editor/_mpl_editor.py +0 -0
  288. /scitex/{vis → fig}/editor/_tkinter_editor.py +0 -0
  289. /scitex/{vis → fig}/editor/flask_editor/__init__.py +0 -0
  290. /scitex/{vis → fig}/editor/flask_editor/_utils.py +0 -0
  291. /scitex/{vis → fig}/io/_directory.py +0 -0
  292. /scitex/{vis → fig}/model/_plot_types.py +0 -0
  293. /scitex/{vis → fig}/utils/_defaults.py +0 -0
  294. /scitex/{vis → fig}/utils/_validate.py +0 -0
  295. {scitex-2.7.0.dist-info → scitex-2.7.3.dist-info}/WHEEL +0 -0
  296. {scitex-2.7.0.dist-info → scitex-2.7.3.dist-info}/entry_points.txt +0 -0
  297. {scitex-2.7.0.dist-info → scitex-2.7.3.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,624 @@
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
+ app = Flask(__name__)
96
+ editor = self
97
+
98
+ @app.route("/")
99
+ def index():
100
+ # Rebuild template each time for hot reload support
101
+ html_template = build_html_template()
102
+
103
+ # Extract figz and panel paths for display
104
+ json_path_str = str(editor.json_path.resolve())
105
+ figz_path = ""
106
+ panel_path = ""
107
+
108
+ # Check if this is inside a figz bundle
109
+ if '.figz.d/' in json_path_str:
110
+ parts = json_path_str.split('.figz.d/')
111
+ figz_path = parts[0] + '.figz.d'
112
+ panel_path = parts[1] if len(parts) > 1 else ""
113
+ elif '.pltz.d/' in json_path_str:
114
+ parts = json_path_str.split('.pltz.d/')
115
+ figz_path = parts[0] + '.pltz.d'
116
+ panel_path = parts[1] if len(parts) > 1 else ""
117
+ else:
118
+ figz_path = json_path_str
119
+
120
+ return render_template_string(
121
+ html_template,
122
+ filename=figz_path,
123
+ panel_path=panel_path,
124
+ overrides=json.dumps(editor.current_overrides),
125
+ )
126
+
127
+ @app.route("/preview")
128
+ def preview():
129
+ """Render figure preview with current overrides (same logic as /update)."""
130
+ from ._renderer import render_preview_with_bboxes
131
+
132
+ # Always use renderer for consistency between initial and updated views
133
+ dark_mode = request.args.get("dark_mode", "false").lower() == "true"
134
+ img_data, bboxes, img_size = render_preview_with_bboxes(
135
+ editor.csv_data, editor.current_overrides,
136
+ metadata=editor.metadata,
137
+ dark_mode=dark_mode,
138
+ )
139
+ return jsonify({
140
+ "image": img_data,
141
+ "bboxes": bboxes,
142
+ "img_size": img_size,
143
+ "has_hitmap": editor.hitmap_path is not None and editor.hitmap_path.exists(),
144
+ "format": "png",
145
+ "panel_info": editor.panel_info,
146
+ })
147
+
148
+ @app.route("/panels")
149
+ def panels():
150
+ """Return all panel images with bboxes for interactive grid view (figz bundles only)."""
151
+ from PIL import Image
152
+ from ._bbox import extract_bboxes_from_metadata, extract_bboxes_from_geometry_px
153
+
154
+ if not editor.panel_info:
155
+ return jsonify({"error": "Not a multi-panel figz bundle"}), 400
156
+
157
+ figz_dir = Path(editor.panel_info["figz_dir"])
158
+ panel_names = editor.panel_info["panels"]
159
+ panel_images = []
160
+
161
+ for panel_name in panel_names:
162
+ panel_dir = figz_dir / panel_name
163
+ panel_data = {"name": panel_name.replace(".pltz.d", ""), "image": None, "bboxes": None, "img_size": None}
164
+
165
+ # Find PNG in exports/ or root
166
+ png_path = None
167
+ exports_dir = panel_dir / "exports"
168
+ if exports_dir.exists():
169
+ for f in exports_dir.glob("*.png"):
170
+ if "_hitmap" not in f.name and "_overview" not in f.name:
171
+ png_path = f
172
+ break
173
+ if not png_path:
174
+ for f in panel_dir.glob("*.png"):
175
+ if "_hitmap" not in f.name and "_overview" not in f.name:
176
+ png_path = f
177
+ break
178
+
179
+ if png_path and png_path.exists():
180
+ with open(png_path, "rb") as f:
181
+ panel_data["image"] = base64.b64encode(f.read()).decode("utf-8")
182
+ img = Image.open(png_path)
183
+ panel_data["width"], panel_data["height"] = img.size
184
+ panel_data["img_size"] = {"width": img.size[0], "height": img.size[1]}
185
+ img.close()
186
+
187
+ # Try to load geometry_px.json from cache (has precise pixel coordinates)
188
+ geometry_path = panel_dir / "cache" / "geometry_px.json"
189
+ if geometry_path.exists():
190
+ import json
191
+ with open(geometry_path) as f:
192
+ geometry_data = json.load(f)
193
+ panel_data["bboxes"] = extract_bboxes_from_geometry_px(
194
+ geometry_data,
195
+ panel_data["img_size"]["width"],
196
+ panel_data["img_size"]["height"]
197
+ )
198
+ else:
199
+ # Fall back to spec.json extraction
200
+ spec_path = panel_dir / "spec.json"
201
+ if spec_path.exists():
202
+ import json
203
+ with open(spec_path) as f:
204
+ panel_metadata = json.load(f)
205
+ panel_data["bboxes"] = extract_bboxes_from_metadata(
206
+ panel_metadata,
207
+ panel_data["img_size"]["width"],
208
+ panel_data["img_size"]["height"]
209
+ )
210
+
211
+ panel_images.append(panel_data)
212
+
213
+ return jsonify({
214
+ "panels": panel_images,
215
+ "count": len(panel_images),
216
+ })
217
+
218
+ @app.route("/switch_panel/<int:panel_index>")
219
+ def switch_panel(panel_index):
220
+ """Switch to a different panel in the figz bundle.
221
+
222
+ Loads the actual PNG from the panel's exports folder instead of re-rendering.
223
+ """
224
+ from PIL import Image
225
+ from .._edit import _load_panel_data
226
+ from ._bbox import extract_bboxes_from_metadata, extract_bboxes_from_geometry_px
227
+
228
+ if not editor.panel_info:
229
+ return jsonify({"error": "Not a multi-panel figz bundle"}), 400
230
+
231
+ panels = editor.panel_info["panels"]
232
+ if panel_index < 0 or panel_index >= len(panels):
233
+ return jsonify({"error": f"Invalid panel index: {panel_index}"}), 400
234
+
235
+ figz_dir = Path(editor.panel_info["figz_dir"])
236
+ panel_name = panels[panel_index]
237
+ panel_dir = figz_dir / panel_name
238
+
239
+ # Load the panel's data
240
+ try:
241
+ panel_data = _load_panel_data(panel_dir)
242
+ if not panel_data:
243
+ return jsonify({"error": f"Could not load panel: {panel_name}"}), 400
244
+
245
+ # Update editor state
246
+ editor.json_path = panel_data["json_path"]
247
+ editor.metadata = panel_data["metadata"]
248
+ editor.csv_data = panel_data.get("csv_data")
249
+ editor.png_path = panel_data.get("png_path")
250
+ editor.hitmap_path = panel_data.get("hitmap_path")
251
+ editor.panel_info["current_index"] = panel_index
252
+
253
+ # Re-extract defaults from new metadata
254
+ from .._defaults import get_scitex_defaults, extract_defaults_from_metadata
255
+ editor.scitex_defaults = get_scitex_defaults()
256
+ editor.metadata_defaults = extract_defaults_from_metadata(editor.metadata)
257
+ editor.current_overrides = copy.deepcopy(editor.scitex_defaults)
258
+ editor.current_overrides.update(editor.metadata_defaults)
259
+ editor.current_overrides.update(editor.manual_overrides)
260
+
261
+ # Load actual PNG from panel instead of re-rendering
262
+ img_data = None
263
+ img_size = {"width": 0, "height": 0}
264
+ png_path = panel_data.get("png_path")
265
+
266
+ if png_path and png_path.exists():
267
+ with open(png_path, "rb") as f:
268
+ img_data = base64.b64encode(f.read()).decode("utf-8")
269
+ img = Image.open(png_path)
270
+ img_size = {"width": img.size[0], "height": img.size[1]}
271
+ img.close()
272
+ else:
273
+ # Fallback: look for any PNG in exports/
274
+ exports_dir = panel_dir / "exports"
275
+ if exports_dir.exists():
276
+ for f in exports_dir.glob("*.png"):
277
+ if "_hitmap" not in f.name and "_overview" not in f.name:
278
+ with open(f, "rb") as pf:
279
+ img_data = base64.b64encode(pf.read()).decode("utf-8")
280
+ img = Image.open(f)
281
+ img_size = {"width": img.size[0], "height": img.size[1]}
282
+ img.close()
283
+ break
284
+
285
+ if not img_data:
286
+ return jsonify({"error": f"No PNG found for panel: {panel_name}"}), 400
287
+
288
+ # Extract bboxes - prefer geometry_px.json for precise coordinates
289
+ bboxes = {}
290
+ geometry_path = panel_dir / "cache" / "geometry_px.json"
291
+ if geometry_path.exists():
292
+ with open(geometry_path) as f:
293
+ geometry_data = json.load(f)
294
+ bboxes = extract_bboxes_from_geometry_px(
295
+ geometry_data,
296
+ img_size["width"],
297
+ img_size["height"],
298
+ )
299
+ else:
300
+ # Fall back to metadata extraction
301
+ bboxes = extract_bboxes_from_metadata(
302
+ editor.metadata,
303
+ img_size["width"],
304
+ img_size["height"],
305
+ )
306
+
307
+ return jsonify({
308
+ "success": True,
309
+ "panel_name": panel_name,
310
+ "panel_index": panel_index,
311
+ "image": img_data,
312
+ "bboxes": bboxes,
313
+ "img_size": img_size,
314
+ "overrides": editor.current_overrides,
315
+ })
316
+ except Exception as e:
317
+ import traceback
318
+ return jsonify({
319
+ "error": f"Failed to switch panel: {str(e)}",
320
+ "traceback": traceback.format_exc(),
321
+ }), 500
322
+
323
+ @app.route("/hitmap")
324
+ def hitmap():
325
+ """Return hitmap PNG for element detection."""
326
+ if editor.hitmap_path and editor.hitmap_path.exists():
327
+ with open(editor.hitmap_path, "rb") as f:
328
+ img_data = base64.b64encode(f.read()).decode("utf-8")
329
+ return jsonify({
330
+ "image": img_data,
331
+ "color_map": editor.color_map,
332
+ })
333
+ return jsonify({"error": "No hitmap available"}), 404
334
+
335
+ @app.route("/color_map")
336
+ def color_map():
337
+ """Return color map for hitmap element identification."""
338
+ return jsonify({
339
+ "color_map": editor.color_map,
340
+ "hit_regions": editor.hit_regions,
341
+ })
342
+
343
+ @app.route("/update", methods=["POST"])
344
+ def update():
345
+ """Update overrides and re-render with updated properties."""
346
+ from ._renderer import render_preview_with_bboxes
347
+
348
+ data = request.json
349
+ editor.current_overrides.update(data.get("overrides", {}))
350
+ editor._user_modified = True
351
+
352
+ # Check if dark mode is requested from POST data
353
+ dark_mode = data.get("dark_mode", False)
354
+
355
+ # Re-render the figure with updated overrides
356
+ img_data, bboxes, img_size = render_preview_with_bboxes(
357
+ editor.csv_data, editor.current_overrides,
358
+ metadata=editor.metadata,
359
+ dark_mode=dark_mode,
360
+ )
361
+ return jsonify({
362
+ "image": img_data,
363
+ "bboxes": bboxes,
364
+ "img_size": img_size,
365
+ "status": "updated",
366
+ })
367
+
368
+ @app.route("/save", methods=["POST"])
369
+ def save():
370
+ """Save to .manual.json."""
371
+ from .._edit import save_manual_overrides
372
+
373
+ try:
374
+ manual_path = save_manual_overrides(
375
+ editor.json_path, editor.current_overrides
376
+ )
377
+ return jsonify({"status": "saved", "path": str(manual_path)})
378
+ except Exception as e:
379
+ return jsonify({"status": "error", "message": str(e)}), 500
380
+
381
+ @app.route("/shutdown", methods=["POST"])
382
+ def shutdown():
383
+ """Shutdown the server."""
384
+ func = request.environ.get("werkzeug.server.shutdown")
385
+ if func is None:
386
+ raise RuntimeError("Not running with Werkzeug Server")
387
+ func()
388
+ return jsonify({"status": "shutdown"})
389
+
390
+ @app.route("/stats")
391
+ def stats():
392
+ """Return statistical test results from figure metadata."""
393
+ stats_data = editor.metadata.get("stats", [])
394
+ stats_summary = editor.metadata.get("stats_summary", None)
395
+ return jsonify({
396
+ "stats": stats_data,
397
+ "stats_summary": stats_summary,
398
+ "has_stats": len(stats_data) > 0,
399
+ })
400
+
401
+ # Open browser after short delay
402
+ def open_browser():
403
+ import time
404
+
405
+ time.sleep(0.5)
406
+ webbrowser.open(f"http://127.0.0.1:{self.port}")
407
+
408
+ threading.Thread(target=open_browser, daemon=True).start()
409
+
410
+ print(f"Starting SciTeX Figure Editor at http://127.0.0.1:{self.port}")
411
+ print("Press Ctrl+C to stop")
412
+
413
+ # Note: use_reloader=False because the reloader re-runs the entire script
414
+ # which causes infinite loops when the demo generates figures
415
+ # Templates are rebuilt on each page refresh anyway
416
+ app.run(host="127.0.0.1", port=self.port, debug=False, use_reloader=False)
417
+
418
+
419
+ def _extract_bboxes_from_metadata(
420
+ metadata: Dict[str, Any],
421
+ display_width: Optional[float] = None,
422
+ display_height: Optional[float] = None
423
+ ) -> Dict[str, Any]:
424
+ """Extract element bounding boxes from pltz metadata.
425
+
426
+ Builds bboxes from selectable_regions in the metadata for click detection.
427
+ This allows the editor to highlight elements when clicked.
428
+
429
+ Coordinate system (new layered format):
430
+ - selectable_regions bbox_px: Already in final image space (figure_px)
431
+ - Display size: Actual displayed image size (PNG pixels or SVG viewBox)
432
+ - Scale = display_size / figure_px (usually 1:1, but may differ for scaled display)
433
+
434
+ Parameters
435
+ ----------
436
+ metadata : dict
437
+ The pltz JSON metadata containing selectable_regions
438
+ display_width : float, optional
439
+ Actual display image width (from PNG size or SVG viewBox)
440
+ display_height : float, optional
441
+ Actual display image height (from PNG size or SVG viewBox)
442
+
443
+ Returns
444
+ -------
445
+ dict
446
+ Mapping of element IDs to their bounding box coordinates (in display pixels)
447
+ """
448
+ bboxes = {}
449
+ selectable = metadata.get("selectable_regions", {})
450
+
451
+ # Figure dimensions from new layered format (bbox_px are in this space)
452
+ figure_px = metadata.get("figure_px", [])
453
+ if isinstance(figure_px, list) and len(figure_px) >= 2:
454
+ fig_width = figure_px[0]
455
+ fig_height = figure_px[1]
456
+ else:
457
+ # Fallback for old format: try hit_regions.path_data.figure
458
+ hit_regions = metadata.get("hit_regions", {})
459
+ path_data = hit_regions.get("path_data", {})
460
+ orig_fig = path_data.get("figure", {})
461
+ fig_width = orig_fig.get("width_px", 944)
462
+ fig_height = orig_fig.get("height_px", 803)
463
+
464
+ # Use actual display dimensions if provided, else use figure_px
465
+ if display_width is None:
466
+ display_width = fig_width
467
+ if display_height is None:
468
+ display_height = fig_height
469
+
470
+ # Scale factor: display / figure_px
471
+ # Usually 1:1 since display is the same PNG, but may differ for scaled display
472
+ scale_x = display_width / fig_width if fig_width > 0 else 1
473
+ scale_y = display_height / fig_height if fig_height > 0 else 1
474
+
475
+ # Helper to convert coords to display pixels
476
+ def to_display_bbox(bbox, is_list=True):
477
+ """Convert bbox to display pixels (apply scaling if display != figure_px).
478
+
479
+ Parameters
480
+ ----------
481
+ bbox : list or dict
482
+ Bbox coordinates [x0, y0, x1, y1] or dict with keys
483
+ is_list : bool
484
+ Whether bbox is a list (True) or dict (False)
485
+ """
486
+ if is_list:
487
+ x0, y0, x1, y1 = bbox[0], bbox[1], bbox[2], bbox[3]
488
+ else:
489
+ x0 = bbox.get("x0", 0)
490
+ y0 = bbox.get("y0", 0)
491
+ x1 = bbox.get("x1", bbox.get("x0", 0) + bbox.get("width", 0))
492
+ y1 = bbox.get("y1", bbox.get("y0", 0) + bbox.get("height", 0))
493
+
494
+ # Scale to display coords (usually 1:1)
495
+ disp_x0 = x0 * scale_x
496
+ disp_x1 = x1 * scale_x
497
+ disp_y0 = y0 * scale_y
498
+ disp_y1 = y1 * scale_y
499
+
500
+ return {
501
+ "x0": disp_x0,
502
+ "y0": disp_y0,
503
+ "x1": disp_x1,
504
+ "y1": disp_y1,
505
+ "x": disp_x0,
506
+ "y": disp_y0,
507
+ "width": disp_x1 - disp_x0,
508
+ "height": disp_y1 - disp_y0,
509
+ }
510
+
511
+ # Extract from selectable_regions.axes
512
+ axes_regions = selectable.get("axes", [])
513
+ for ax_idx, ax in enumerate(axes_regions):
514
+ ax_key = f"ax_{ax_idx:02d}"
515
+
516
+ # Title
517
+ title = ax.get("title", {})
518
+ if title and "bbox_px" in title:
519
+ bbox_disp = to_display_bbox(title["bbox_px"])
520
+ bboxes[f"{ax_key}_title"] = {
521
+ **bbox_disp,
522
+ "type": "title",
523
+ "text": title.get("text", ""),
524
+ }
525
+
526
+ # X label
527
+ xlabel = ax.get("xlabel", {})
528
+ if xlabel and "bbox_px" in xlabel:
529
+ bbox_disp = to_display_bbox(xlabel["bbox_px"])
530
+ bboxes[f"{ax_key}_xlabel"] = {
531
+ **bbox_disp,
532
+ "type": "xlabel",
533
+ "text": xlabel.get("text", ""),
534
+ }
535
+
536
+ # Y label
537
+ ylabel = ax.get("ylabel", {})
538
+ if ylabel and "bbox_px" in ylabel:
539
+ bbox_disp = to_display_bbox(ylabel["bbox_px"])
540
+ bboxes[f"{ax_key}_ylabel"] = {
541
+ **bbox_disp,
542
+ "type": "ylabel",
543
+ "text": ylabel.get("text", ""),
544
+ }
545
+
546
+ # Legend
547
+ legend = ax.get("legend", {})
548
+ if legend and "bbox_px" in legend:
549
+ bbox_disp = to_display_bbox(legend["bbox_px"])
550
+ bboxes[f"{ax_key}_legend"] = {
551
+ **bbox_disp,
552
+ "type": "legend",
553
+ }
554
+
555
+ # X-axis spine
556
+ xaxis = ax.get("xaxis", {})
557
+ if xaxis:
558
+ spine = xaxis.get("spine", {})
559
+ if spine and "bbox_px" in spine:
560
+ bbox_disp = to_display_bbox(spine["bbox_px"])
561
+ bboxes[f"{ax_key}_xaxis_spine"] = {
562
+ **bbox_disp,
563
+ "type": "xaxis",
564
+ }
565
+
566
+ # Y-axis spine
567
+ yaxis = ax.get("yaxis", {})
568
+ if yaxis:
569
+ spine = yaxis.get("spine", {})
570
+ if spine and "bbox_px" in spine:
571
+ bbox_disp = to_display_bbox(spine["bbox_px"])
572
+ bboxes[f"{ax_key}_yaxis_spine"] = {
573
+ **bbox_disp,
574
+ "type": "yaxis",
575
+ }
576
+
577
+ # Extract traces from artists (top-level in new format, or hit_regions.path_data in old)
578
+ artists = metadata.get("artists", [])
579
+ if not artists:
580
+ # Fallback for old format
581
+ hit_regions = metadata.get("hit_regions", {})
582
+ path_data = hit_regions.get("path_data", {})
583
+ artists = path_data.get("artists", [])
584
+
585
+ for artist in artists:
586
+ artist_id = artist.get("id", 0)
587
+ artist_type = artist.get("type", "line")
588
+ bbox_px = artist.get("bbox_px", {})
589
+ if bbox_px:
590
+ bbox_disp = to_display_bbox(bbox_px, is_list=False)
591
+ trace_entry = {
592
+ **bbox_disp,
593
+ "type": artist_type,
594
+ "label": artist.get("label", f"Trace {artist_id}"),
595
+ "element_type": artist_type,
596
+ }
597
+
598
+ # Include scaled path points for line proximity detection
599
+ path_px = artist.get("path_px", [])
600
+ if path_px:
601
+ scaled_points = [
602
+ [pt[0] * scale_x, pt[1] * scale_y]
603
+ for pt in path_px if len(pt) >= 2
604
+ ]
605
+ trace_entry["points"] = scaled_points
606
+
607
+ bboxes[f"trace_{artist_id}"] = trace_entry
608
+
609
+ # Add metadata for JavaScript to understand the coordinate system
610
+ bboxes["_meta"] = {
611
+ "display_width": display_width,
612
+ "display_height": display_height,
613
+ "figure_px_width": fig_width,
614
+ "figure_px_height": fig_height,
615
+ "scale_x": scale_x,
616
+ "scale_y": scale_y,
617
+ # Note: With new layered format, bbox_px are already in final image space
618
+ # so scale is typically 1:1 (unless display is resized)
619
+ }
620
+
621
+ return bboxes
622
+
623
+
624
+ # EOF
@@ -206,6 +206,8 @@ def plot_from_recipe(
206
206
  _render_imshow(ax, df, data_ref, kwargs)
207
207
  elif method == "contour":
208
208
  _render_contour(ax, df, data_ref, kwargs)
209
+ elif method == "contourf":
210
+ _render_contourf(ax, df, data_ref, kwargs)
209
211
  elif method in ("stx_shaded_line", "stx_fillv", "stx_violin",
210
212
  "stx_box", "stx_rectangle", "stx_raster"):
211
213
  _render_stx_method(ax, df, method, data_ref, kwargs)
@@ -393,6 +395,26 @@ def _render_contour(ax, df, data_ref, kwargs):
393
395
  ax.contour(X, Y, Z, **kwargs)
394
396
 
395
397
 
398
+ def _render_contourf(ax, df, data_ref, kwargs):
399
+ """Render filled contour plot."""
400
+ x_col = data_ref.get("x", "")
401
+ y_col = data_ref.get("y", "")
402
+ z_col = data_ref.get("z", "")
403
+
404
+ x = _get_column_data(df, x_col)
405
+ y = _get_column_data(df, y_col)
406
+ z = _get_column_data(df, z_col)
407
+
408
+ if x is not None and y is not None and z is not None:
409
+ # Assume data is on a grid - reconstruct
410
+ n = int(np.sqrt(len(x)))
411
+ if n * n == len(x):
412
+ X = x.reshape(n, n)
413
+ Y = y.reshape(n, n)
414
+ Z = z.reshape(n, n)
415
+ ax.contourf(X, Y, Z, **kwargs)
416
+
417
+
396
418
  def _render_stx_method(ax, df, method, data_ref, kwargs):
397
419
  """Render scitex-specific methods (shaded_line, fillv, etc.)."""
398
420
  # These are custom methods - for now, skip or implement basic versions
@@ -452,7 +474,18 @@ def _render_generic(ax, df, method, data_ref, kwargs, linewidth):
452
474
  y = _get_column_data(df, y_col)
453
475
 
454
476
  if x is not None and y is not None and len(x) == len(y):
455
- ax.plot(x, y, linewidth=linewidth, **kwargs)
477
+ # Filter out kwargs that are not valid for ax.plot()
478
+ invalid_plot_kwargs = {
479
+ 'levels', 'extend', 'origin', 'extent', 'aspect',
480
+ 'norm', 'vmin', 'vmax', 'interpolation', 'filternorm',
481
+ 'filterrad', 'resample', 'bins', 'range', 'density',
482
+ 'weights', 'cumulative', 'bottom', 'histtype', 'align',
483
+ 'orientation', 'rwidth', 'log', 'stacked', 'data',
484
+ 'width', 'height', 'edgecolors', 's', 'c', 'facecolors',
485
+ }
486
+ filtered_kwargs = {k: v for k, v in kwargs.items()
487
+ if k not in invalid_plot_kwargs}
488
+ ax.plot(x, y, linewidth=linewidth, **filtered_kwargs)
456
489
 
457
490
 
458
491
  def _plot_with_traces(
@@ -469,11 +502,12 @@ def _plot_with_traces(
469
502
  ):
470
503
  """Plot using trace information from overrides."""
471
504
  for trace in traces:
505
+ # Support both old format (csv_columns.x/y) and new format (x_col/y_col)
472
506
  csv_cols = trace.get("csv_columns", {})
473
- x_col = csv_cols.get("x")
474
- y_col = csv_cols.get("y")
507
+ x_col = csv_cols.get("x") or trace.get("x_col")
508
+ y_col = csv_cols.get("y") or trace.get("y_col")
475
509
 
476
- if x_col in df.columns and y_col in df.columns:
510
+ if x_col and y_col and x_col in df.columns and y_col in df.columns:
477
511
  ax.plot(
478
512
  df[x_col],
479
513
  df[y_col],