scitex 2.5.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 (1179) hide show
  1. scitex/__init__.py +25 -10
  2. scitex/__main__.py +2 -1
  3. scitex/__version__.py +1 -1
  4. scitex/_optional_deps.py +13 -20
  5. scitex/ai/__init__.py +5 -0
  6. scitex/ai/_gen_ai/_Anthropic.py +3 -1
  7. scitex/ai/_gen_ai/_BaseGenAI.py +3 -2
  8. scitex/ai/_gen_ai/_DeepSeek.py +1 -1
  9. scitex/ai/_gen_ai/_Google.py +3 -2
  10. scitex/ai/_gen_ai/_Llama.py +4 -2
  11. scitex/ai/_gen_ai/_OpenAI.py +3 -1
  12. scitex/ai/_gen_ai/_PARAMS.py +1 -0
  13. scitex/ai/_gen_ai/_Perplexity.py +3 -1
  14. scitex/ai/_gen_ai/__init__.py +1 -0
  15. scitex/ai/_gen_ai/_format_output_func.py +3 -1
  16. scitex/ai/classification/CrossValidationExperiment.py +8 -14
  17. scitex/ai/classification/examples/timeseries_cv_demo.py +128 -112
  18. scitex/ai/classification/reporters/_BaseClassificationReporter.py +2 -0
  19. scitex/ai/classification/reporters/_ClassificationReporter.py +30 -45
  20. scitex/ai/classification/reporters/_MultiClassificationReporter.py +8 -11
  21. scitex/ai/classification/reporters/_SingleClassificationReporter.py +126 -182
  22. scitex/ai/classification/reporters/__init__.py +1 -1
  23. scitex/ai/classification/reporters/reporter_utils/_Plotter.py +213 -119
  24. scitex/ai/classification/reporters/reporter_utils/__init__.py +28 -36
  25. scitex/ai/classification/reporters/reporter_utils/aggregation.py +125 -143
  26. scitex/ai/classification/reporters/reporter_utils/data_models.py +128 -120
  27. scitex/ai/classification/reporters/reporter_utils/reporting.py +507 -340
  28. scitex/ai/classification/reporters/reporter_utils/storage.py +4 -1
  29. scitex/ai/classification/reporters/reporter_utils/validation.py +141 -154
  30. scitex/ai/classification/timeseries/_TimeSeriesBlockingSplit.py +204 -129
  31. scitex/ai/classification/timeseries/_TimeSeriesCalendarSplit.py +215 -171
  32. scitex/ai/classification/timeseries/_TimeSeriesMetadata.py +17 -17
  33. scitex/ai/classification/timeseries/_TimeSeriesSlidingWindowSplit.py +67 -143
  34. scitex/ai/classification/timeseries/_TimeSeriesSlidingWindowSplit_v01-not-using-n_splits.py +67 -143
  35. scitex/ai/classification/timeseries/_TimeSeriesStrategy.py +12 -13
  36. scitex/ai/classification/timeseries/_TimeSeriesStratifiedSplit.py +231 -144
  37. scitex/ai/classification/timeseries/__init__.py +2 -4
  38. scitex/ai/classification/timeseries/_normalize_timestamp.py +3 -0
  39. scitex/ai/clustering/_pca.py +0 -1
  40. scitex/ai/clustering/_umap.py +1 -2
  41. scitex/ai/feature_extraction/__init__.py +10 -8
  42. scitex/ai/feature_extraction/vit.py +0 -1
  43. scitex/ai/feature_selection/feature_selection.py +3 -8
  44. scitex/ai/metrics/_calc_conf_mat.py +2 -0
  45. scitex/ai/metrics/_calc_feature_importance.py +3 -7
  46. scitex/ai/metrics/_calc_pre_rec_auc.py +5 -5
  47. scitex/ai/metrics/_calc_roc_auc.py +4 -2
  48. scitex/ai/metrics/_calc_seizure_prediction_metrics.py +35 -20
  49. scitex/ai/metrics/_calc_silhouette_score.py +1 -3
  50. scitex/ai/optim/Ranger_Deep_Learning_Optimizer/ranger/ranger.py +0 -3
  51. scitex/ai/optim/Ranger_Deep_Learning_Optimizer/ranger/ranger2020.py +0 -3
  52. scitex/ai/optim/Ranger_Deep_Learning_Optimizer/ranger/ranger913A.py +0 -3
  53. scitex/ai/optim/_optimizers.py +1 -1
  54. scitex/ai/plt/__init__.py +6 -1
  55. scitex/ai/plt/_plot_feature_importance.py +1 -3
  56. scitex/ai/plt/_plot_learning_curve.py +9 -24
  57. scitex/ai/plt/_plot_optuna_study.py +4 -3
  58. scitex/ai/plt/_plot_pre_rec_curve.py +9 -15
  59. scitex/ai/plt/_plot_roc_curve.py +6 -8
  60. scitex/ai/plt/_stx_conf_mat.py +121 -122
  61. scitex/ai/sampling/undersample.py +3 -2
  62. scitex/ai/sklearn/__init__.py +2 -2
  63. scitex/ai/training/_LearningCurveLogger.py +23 -10
  64. scitex/ai/utils/_check_params.py +0 -1
  65. scitex/audio/README.md +52 -0
  66. scitex/audio/__init__.py +384 -0
  67. scitex/audio/__main__.py +129 -0
  68. scitex/audio/_tts.py +334 -0
  69. scitex/audio/engines/__init__.py +44 -0
  70. scitex/audio/engines/base.py +275 -0
  71. scitex/audio/engines/elevenlabs_engine.py +143 -0
  72. scitex/audio/engines/gtts_engine.py +162 -0
  73. scitex/audio/engines/pyttsx3_engine.py +131 -0
  74. scitex/audio/mcp_server.py +757 -0
  75. scitex/benchmark/__init__.py +15 -25
  76. scitex/benchmark/benchmark.py +124 -117
  77. scitex/benchmark/monitor.py +117 -107
  78. scitex/benchmark/profiler.py +61 -58
  79. scitex/bridge/__init__.py +110 -0
  80. scitex/bridge/_helpers.py +149 -0
  81. scitex/bridge/_plt_vis.py +529 -0
  82. scitex/bridge/_protocol.py +283 -0
  83. scitex/bridge/_stats_plt.py +261 -0
  84. scitex/bridge/_stats_vis.py +265 -0
  85. scitex/browser/__init__.py +0 -2
  86. scitex/browser/auth/__init__.py +0 -0
  87. scitex/browser/auth/google.py +16 -11
  88. scitex/browser/automation/CookieHandler.py +2 -3
  89. scitex/browser/collaboration/__init__.py +3 -0
  90. scitex/browser/collaboration/auth_helpers.py +3 -1
  91. scitex/browser/collaboration/collaborative_agent.py +2 -0
  92. scitex/browser/collaboration/interactive_panel.py +2 -2
  93. scitex/browser/collaboration/shared_session.py +20 -11
  94. scitex/browser/collaboration/standard_interactions.py +1 -0
  95. scitex/browser/core/BrowserMixin.py +12 -30
  96. scitex/browser/core/ChromeProfileManager.py +9 -24
  97. scitex/browser/debugging/_browser_logger.py +15 -25
  98. scitex/browser/debugging/_failure_capture.py +9 -2
  99. scitex/browser/debugging/_highlight_element.py +15 -6
  100. scitex/browser/debugging/_show_grid.py +5 -6
  101. scitex/browser/debugging/_sync_session.py +4 -3
  102. scitex/browser/debugging/_test_monitor.py +14 -5
  103. scitex/browser/debugging/_visual_cursor.py +46 -35
  104. scitex/browser/interaction/click_center.py +4 -3
  105. scitex/browser/interaction/click_with_fallbacks.py +7 -10
  106. scitex/browser/interaction/close_popups.py +79 -66
  107. scitex/browser/interaction/fill_with_fallbacks.py +8 -8
  108. scitex/browser/pdf/__init__.py +3 -1
  109. scitex/browser/pdf/click_download_for_chrome_pdf_viewer.py +11 -10
  110. scitex/browser/pdf/detect_chrome_pdf_viewer.py +3 -6
  111. scitex/browser/remote/CaptchaHandler.py +109 -96
  112. scitex/browser/remote/ZenRowsAPIClient.py +91 -97
  113. scitex/browser/remote/ZenRowsBrowserManager.py +138 -112
  114. scitex/browser/stealth/HumanBehavior.py +4 -9
  115. scitex/browser/stealth/StealthManager.py +11 -26
  116. scitex/capture/__init__.py +17 -17
  117. scitex/capture/__main__.py +2 -3
  118. scitex/capture/capture.py +23 -51
  119. scitex/capture/cli.py +14 -39
  120. scitex/capture/gif.py +5 -9
  121. scitex/capture/mcp_server.py +7 -20
  122. scitex/capture/session.py +4 -3
  123. scitex/capture/utils.py +18 -53
  124. scitex/cli/__init__.py +1 -1
  125. scitex/cli/cloud.py +158 -116
  126. scitex/cli/config.py +224 -0
  127. scitex/cli/main.py +41 -40
  128. scitex/cli/scholar.py +60 -27
  129. scitex/cli/security.py +14 -20
  130. scitex/cli/web.py +87 -90
  131. scitex/cli/writer.py +51 -45
  132. scitex/cloud/__init__.py +14 -11
  133. scitex/cloud/_matplotlib_hook.py +6 -6
  134. scitex/config/README.md +313 -0
  135. scitex/config/{PriorityConfig.py → _PriorityConfig.py} +114 -17
  136. scitex/config/_ScitexConfig.py +319 -0
  137. scitex/config/__init__.py +41 -9
  138. scitex/config/_paths.py +325 -0
  139. scitex/config/default.yaml +81 -0
  140. scitex/context/_suppress_output.py +2 -3
  141. scitex/db/_BaseMixins/_BaseBackupMixin.py +3 -1
  142. scitex/db/_BaseMixins/_BaseBatchMixin.py +3 -1
  143. scitex/db/_BaseMixins/_BaseBlobMixin.py +3 -1
  144. scitex/db/_BaseMixins/_BaseImportExportMixin.py +1 -3
  145. scitex/db/_BaseMixins/_BaseIndexMixin.py +3 -1
  146. scitex/db/_BaseMixins/_BaseMaintenanceMixin.py +1 -3
  147. scitex/db/_BaseMixins/_BaseQueryMixin.py +3 -1
  148. scitex/db/_BaseMixins/_BaseRowMixin.py +3 -1
  149. scitex/db/_BaseMixins/_BaseTableMixin.py +3 -1
  150. scitex/db/_BaseMixins/_BaseTransactionMixin.py +1 -3
  151. scitex/db/_BaseMixins/__init__.py +1 -1
  152. scitex/db/__init__.py +9 -1
  153. scitex/db/__main__.py +8 -21
  154. scitex/db/_check_health.py +15 -31
  155. scitex/db/_delete_duplicates.py +7 -4
  156. scitex/db/_inspect.py +22 -38
  157. scitex/db/_inspect_optimized.py +89 -85
  158. scitex/db/_postgresql/_PostgreSQL.py +0 -1
  159. scitex/db/_postgresql/_PostgreSQLMixins/_BlobMixin.py +3 -1
  160. scitex/db/_postgresql/_PostgreSQLMixins/_ConnectionMixin.py +1 -3
  161. scitex/db/_postgresql/_PostgreSQLMixins/_ImportExportMixin.py +1 -3
  162. scitex/db/_postgresql/_PostgreSQLMixins/_MaintenanceMixin.py +1 -4
  163. scitex/db/_postgresql/_PostgreSQLMixins/_QueryMixin.py +3 -3
  164. scitex/db/_postgresql/_PostgreSQLMixins/_RowMixin.py +3 -1
  165. scitex/db/_postgresql/_PostgreSQLMixins/_TransactionMixin.py +1 -3
  166. scitex/db/_postgresql/__init__.py +1 -1
  167. scitex/db/_sqlite3/_SQLite3.py +2 -4
  168. scitex/db/_sqlite3/_SQLite3Mixins/_ArrayMixin.py +11 -12
  169. scitex/db/_sqlite3/_SQLite3Mixins/_ArrayMixin_v01-need-_hash-col.py +19 -14
  170. scitex/db/_sqlite3/_SQLite3Mixins/_BatchMixin.py +3 -1
  171. scitex/db/_sqlite3/_SQLite3Mixins/_BlobMixin.py +7 -7
  172. scitex/db/_sqlite3/_SQLite3Mixins/_ColumnMixin.py +118 -111
  173. scitex/db/_sqlite3/_SQLite3Mixins/_ConnectionMixin.py +8 -10
  174. scitex/db/_sqlite3/_SQLite3Mixins/_GitMixin.py +17 -45
  175. scitex/db/_sqlite3/_SQLite3Mixins/_ImportExportMixin.py +1 -3
  176. scitex/db/_sqlite3/_SQLite3Mixins/_IndexMixin.py +3 -1
  177. scitex/db/_sqlite3/_SQLite3Mixins/_QueryMixin.py +3 -4
  178. scitex/db/_sqlite3/_SQLite3Mixins/_RowMixin.py +9 -9
  179. scitex/db/_sqlite3/_SQLite3Mixins/_TableMixin.py +18 -11
  180. scitex/db/_sqlite3/_SQLite3Mixins/__init__.py +1 -0
  181. scitex/db/_sqlite3/__init__.py +1 -1
  182. scitex/db/_sqlite3/_delete_duplicates.py +13 -11
  183. scitex/decorators/__init__.py +29 -4
  184. scitex/decorators/_auto_order.py +43 -43
  185. scitex/decorators/_batch_fn.py +12 -6
  186. scitex/decorators/_cache_disk.py +8 -9
  187. scitex/decorators/_cache_disk_async.py +8 -7
  188. scitex/decorators/_combined.py +19 -13
  189. scitex/decorators/_converters.py +16 -3
  190. scitex/decorators/_deprecated.py +32 -22
  191. scitex/decorators/_numpy_fn.py +18 -4
  192. scitex/decorators/_pandas_fn.py +17 -5
  193. scitex/decorators/_signal_fn.py +17 -3
  194. scitex/decorators/_torch_fn.py +32 -15
  195. scitex/decorators/_xarray_fn.py +23 -9
  196. scitex/dev/_analyze_code_flow.py +0 -2
  197. scitex/dev/plt/__init__.py +272 -0
  198. scitex/dev/plt/plot_mpl_axhline.py +28 -0
  199. scitex/dev/plt/plot_mpl_axhspan.py +28 -0
  200. scitex/dev/plt/plot_mpl_axvline.py +28 -0
  201. scitex/dev/plt/plot_mpl_axvspan.py +28 -0
  202. scitex/dev/plt/plot_mpl_bar.py +29 -0
  203. scitex/dev/plt/plot_mpl_barh.py +29 -0
  204. scitex/dev/plt/plot_mpl_boxplot.py +28 -0
  205. scitex/dev/plt/plot_mpl_contour.py +31 -0
  206. scitex/dev/plt/plot_mpl_contourf.py +31 -0
  207. scitex/dev/plt/plot_mpl_errorbar.py +30 -0
  208. scitex/dev/plt/plot_mpl_eventplot.py +28 -0
  209. scitex/dev/plt/plot_mpl_fill.py +30 -0
  210. scitex/dev/plt/plot_mpl_fill_between.py +31 -0
  211. scitex/dev/plt/plot_mpl_hexbin.py +28 -0
  212. scitex/dev/plt/plot_mpl_hist.py +28 -0
  213. scitex/dev/plt/plot_mpl_hist2d.py +28 -0
  214. scitex/dev/plt/plot_mpl_imshow.py +29 -0
  215. scitex/dev/plt/plot_mpl_pcolormesh.py +31 -0
  216. scitex/dev/plt/plot_mpl_pie.py +29 -0
  217. scitex/dev/plt/plot_mpl_plot.py +29 -0
  218. scitex/dev/plt/plot_mpl_quiver.py +31 -0
  219. scitex/dev/plt/plot_mpl_scatter.py +28 -0
  220. scitex/dev/plt/plot_mpl_stackplot.py +31 -0
  221. scitex/dev/plt/plot_mpl_stem.py +29 -0
  222. scitex/dev/plt/plot_mpl_step.py +29 -0
  223. scitex/dev/plt/plot_mpl_violinplot.py +28 -0
  224. scitex/dev/plt/plot_sns_barplot.py +29 -0
  225. scitex/dev/plt/plot_sns_boxplot.py +29 -0
  226. scitex/dev/plt/plot_sns_heatmap.py +28 -0
  227. scitex/dev/plt/plot_sns_histplot.py +29 -0
  228. scitex/dev/plt/plot_sns_kdeplot.py +29 -0
  229. scitex/dev/plt/plot_sns_lineplot.py +31 -0
  230. scitex/dev/plt/plot_sns_scatterplot.py +29 -0
  231. scitex/dev/plt/plot_sns_stripplot.py +29 -0
  232. scitex/dev/plt/plot_sns_swarmplot.py +29 -0
  233. scitex/dev/plt/plot_sns_violinplot.py +29 -0
  234. scitex/dev/plt/plot_stx_bar.py +29 -0
  235. scitex/dev/plt/plot_stx_barh.py +29 -0
  236. scitex/dev/plt/plot_stx_box.py +28 -0
  237. scitex/dev/plt/plot_stx_boxplot.py +28 -0
  238. scitex/dev/plt/plot_stx_conf_mat.py +28 -0
  239. scitex/dev/plt/plot_stx_contour.py +31 -0
  240. scitex/dev/plt/plot_stx_ecdf.py +28 -0
  241. scitex/dev/plt/plot_stx_errorbar.py +30 -0
  242. scitex/dev/plt/plot_stx_fill_between.py +31 -0
  243. scitex/dev/plt/plot_stx_fillv.py +28 -0
  244. scitex/dev/plt/plot_stx_heatmap.py +28 -0
  245. scitex/dev/plt/plot_stx_image.py +28 -0
  246. scitex/dev/plt/plot_stx_imshow.py +28 -0
  247. scitex/dev/plt/plot_stx_joyplot.py +28 -0
  248. scitex/dev/plt/plot_stx_kde.py +28 -0
  249. scitex/dev/plt/plot_stx_line.py +28 -0
  250. scitex/dev/plt/plot_stx_mean_ci.py +28 -0
  251. scitex/dev/plt/plot_stx_mean_std.py +28 -0
  252. scitex/dev/plt/plot_stx_median_iqr.py +28 -0
  253. scitex/dev/plt/plot_stx_raster.py +28 -0
  254. scitex/dev/plt/plot_stx_rectangle.py +28 -0
  255. scitex/dev/plt/plot_stx_scatter.py +29 -0
  256. scitex/dev/plt/plot_stx_shaded_line.py +29 -0
  257. scitex/dev/plt/plot_stx_violin.py +28 -0
  258. scitex/dev/plt/plot_stx_violinplot.py +28 -0
  259. scitex/dict/_DotDict.py +15 -19
  260. scitex/dict/_flatten.py +1 -0
  261. scitex/dict/_listed_dict.py +1 -0
  262. scitex/dict/_pop_keys.py +1 -0
  263. scitex/dict/_replace.py +1 -0
  264. scitex/dict/_safe_merge.py +1 -0
  265. scitex/dict/_to_str.py +2 -3
  266. scitex/dsp/__init__.py +13 -4
  267. scitex/dsp/_crop.py +3 -1
  268. scitex/dsp/_detect_ripples.py +3 -1
  269. scitex/dsp/_modulation_index.py +3 -1
  270. scitex/dsp/_time.py +3 -1
  271. scitex/dsp/_wavelet.py +0 -1
  272. scitex/dsp/example.py +0 -5
  273. scitex/dsp/filt.py +4 -0
  274. scitex/dsp/utils/__init__.py +4 -1
  275. scitex/dsp/utils/pac.py +3 -3
  276. scitex/dt/_normalize_timestamp.py +4 -1
  277. scitex/errors.py +3 -6
  278. scitex/etc/__init__.py +1 -1
  279. scitex/fig/__init__.py +352 -0
  280. scitex/{vis → fig}/backend/__init__.py +3 -3
  281. scitex/{vis/backend/export.py → fig/backend/_export.py} +1 -1
  282. scitex/{vis/backend/parser.py → fig/backend/_parser.py} +2 -4
  283. scitex/{vis/backend/render.py → fig/backend/_render.py} +1 -1
  284. scitex/{vis → fig}/canvas.py +16 -4
  285. scitex/{vis → fig}/editor/__init__.py +0 -0
  286. scitex/{vis → fig}/editor/_dearpygui_editor.py +450 -304
  287. scitex/fig/editor/_defaults.py +300 -0
  288. scitex/fig/editor/_edit.py +751 -0
  289. scitex/{vis → fig}/editor/_flask_editor.py +8 -8
  290. scitex/{vis → fig}/editor/_mpl_editor.py +63 -48
  291. scitex/{vis → fig}/editor/_qt_editor.py +391 -160
  292. scitex/{vis → fig}/editor/_tkinter_editor.py +146 -89
  293. scitex/fig/editor/flask_editor/__init__.py +21 -0
  294. scitex/fig/editor/flask_editor/_bbox.py +1276 -0
  295. scitex/fig/editor/flask_editor/_core.py +624 -0
  296. scitex/fig/editor/flask_editor/_plotter.py +601 -0
  297. scitex/fig/editor/flask_editor/_renderer.py +739 -0
  298. scitex/{vis/editor/flask_editor/utils.py → fig/editor/flask_editor/_utils.py} +13 -14
  299. scitex/{vis → fig}/editor/flask_editor/templates/__init__.py +6 -6
  300. scitex/fig/editor/flask_editor/templates/_html.py +834 -0
  301. scitex/fig/editor/flask_editor/templates/_scripts.py +3136 -0
  302. scitex/fig/editor/flask_editor/templates/_styles.py +1346 -0
  303. scitex/{vis → fig}/io/__init__.py +18 -6
  304. scitex/fig/io/_bundle.py +973 -0
  305. scitex/{vis/io/canvas.py → fig/io/_canvas.py} +9 -5
  306. scitex/{vis/io/data.py → fig/io/_data.py} +14 -10
  307. scitex/{vis/io/directory.py → fig/io/_directory.py} +7 -4
  308. scitex/{vis/io/export.py → fig/io/_export.py} +16 -13
  309. scitex/{vis/io/load.py → fig/io/_load.py} +2 -2
  310. scitex/{vis/io/panel.py → fig/io/_panel.py} +22 -14
  311. scitex/{vis/io/save.py → fig/io/_save.py} +1 -1
  312. scitex/{vis → fig}/model/__init__.py +8 -8
  313. scitex/{vis/model/annotations.py → fig/model/_annotations.py} +3 -5
  314. scitex/{vis/model/axes.py → fig/model/_axes.py} +2 -2
  315. scitex/{vis/model/figure.py → fig/model/_figure.py} +1 -1
  316. scitex/{vis/model/guides.py → fig/model/_guides.py} +2 -2
  317. scitex/{vis/model/plot.py → fig/model/_plot.py} +3 -5
  318. scitex/{vis/model/plot_types.py → fig/model/_plot_types.py} +0 -0
  319. scitex/{vis/model/styles.py → fig/model/_styles.py} +1 -1
  320. scitex/{vis → fig}/utils/__init__.py +3 -3
  321. scitex/{vis/utils/defaults.py → fig/utils/_defaults.py} +1 -2
  322. scitex/{vis/utils/validate.py → fig/utils/_validate.py} +3 -9
  323. scitex/gen/_DimHandler.py +6 -6
  324. scitex/gen/__init__.py +5 -1
  325. scitex/gen/_deprecated_close.py +1 -0
  326. scitex/gen/_deprecated_start.py +5 -3
  327. scitex/gen/_detect_environment.py +44 -41
  328. scitex/gen/_detect_notebook_path.py +51 -47
  329. scitex/gen/_embed.py +1 -1
  330. scitex/gen/_get_notebook_path.py +81 -62
  331. scitex/gen/_inspect_module.py +0 -1
  332. scitex/gen/_norm.py +16 -7
  333. scitex/gen/_norm_cache.py +78 -65
  334. scitex/gen/_print_config.py +0 -3
  335. scitex/gen/_src.py +2 -3
  336. scitex/gen/_title_case.py +3 -2
  337. scitex/gen/_to_even.py +8 -8
  338. scitex/gen/_transpose.py +3 -3
  339. scitex/gen/misc.py +0 -3
  340. scitex/gists/_SigMacro_processFigure_S.py +2 -2
  341. scitex/gists/_SigMacro_toBlue.py +2 -2
  342. scitex/gists/__init__.py +4 -1
  343. scitex/git/_branch.py +19 -11
  344. scitex/git/_clone.py +23 -15
  345. scitex/git/_commit.py +10 -12
  346. scitex/git/_init.py +15 -38
  347. scitex/git/_remote.py +9 -3
  348. scitex/git/_result.py +3 -0
  349. scitex/git/_retry.py +2 -5
  350. scitex/git/_types.py +4 -0
  351. scitex/git/_validation.py +8 -8
  352. scitex/git/_workflow.py +4 -4
  353. scitex/io/__init__.py +12 -27
  354. scitex/io/_bundle.py +434 -0
  355. scitex/io/_flush.py +5 -2
  356. scitex/io/_glob.py +2 -2
  357. scitex/io/_json2md.py +3 -3
  358. scitex/io/_load.py +104 -8
  359. scitex/io/_load_cache.py +71 -71
  360. scitex/io/_load_configs.py +2 -3
  361. scitex/io/_load_modules/_H5Explorer.py +11 -14
  362. scitex/io/_load_modules/_ZarrExplorer.py +3 -3
  363. scitex/io/_load_modules/_bibtex.py +62 -63
  364. scitex/io/_load_modules/_canvas.py +6 -11
  365. scitex/io/_load_modules/_catboost.py +7 -2
  366. scitex/io/_load_modules/_hdf5.py +2 -0
  367. scitex/io/_load_modules/_image.py +7 -4
  368. scitex/io/_load_modules/_matlab.py +3 -1
  369. scitex/io/_load_modules/_optuna.py +0 -1
  370. scitex/io/_load_modules/_pdf.py +38 -29
  371. scitex/io/_load_modules/_sqlite3.py +1 -0
  372. scitex/io/_load_modules/_txt.py +6 -2
  373. scitex/io/_load_modules/_xml.py +9 -9
  374. scitex/io/_load_modules/_zarr.py +12 -10
  375. scitex/io/_metadata.py +34 -285
  376. scitex/io/_metadata_modules/__init__.py +46 -0
  377. scitex/io/_metadata_modules/_embed.py +70 -0
  378. scitex/io/_metadata_modules/_read.py +64 -0
  379. scitex/io/_metadata_modules/_utils.py +79 -0
  380. scitex/io/_metadata_modules/embed_metadata_jpeg.py +74 -0
  381. scitex/io/_metadata_modules/embed_metadata_pdf.py +53 -0
  382. scitex/io/_metadata_modules/embed_metadata_png.py +26 -0
  383. scitex/io/_metadata_modules/embed_metadata_svg.py +62 -0
  384. scitex/io/_metadata_modules/read_metadata_jpeg.py +57 -0
  385. scitex/io/_metadata_modules/read_metadata_pdf.py +51 -0
  386. scitex/io/_metadata_modules/read_metadata_png.py +39 -0
  387. scitex/io/_metadata_modules/read_metadata_svg.py +44 -0
  388. scitex/io/_qr_utils.py +21 -14
  389. scitex/io/_save.py +755 -80
  390. scitex/io/_save_modules/__init__.py +7 -2
  391. scitex/io/_save_modules/_bibtex.py +66 -61
  392. scitex/io/_save_modules/_canvas.py +8 -9
  393. scitex/io/_save_modules/_catboost.py +2 -2
  394. scitex/io/_save_modules/_csv.py +4 -4
  395. scitex/io/_save_modules/_excel.py +5 -9
  396. scitex/io/_save_modules/_hdf5.py +9 -21
  397. scitex/io/_save_modules/_html.py +5 -5
  398. scitex/io/_save_modules/_image.py +107 -14
  399. scitex/io/_save_modules/_joblib.py +2 -2
  400. scitex/io/_save_modules/_json.py +51 -6
  401. scitex/io/_save_modules/_listed_dfs_as_csv.py +2 -1
  402. scitex/io/_save_modules/_listed_scalars_as_csv.py +2 -1
  403. scitex/io/_save_modules/_matlab.py +2 -2
  404. scitex/io/_save_modules/_numpy.py +6 -8
  405. scitex/io/_save_modules/_pickle.py +4 -4
  406. scitex/io/_save_modules/_plotly.py +3 -3
  407. scitex/io/_save_modules/_tex.py +30 -29
  408. scitex/io/_save_modules/_text.py +2 -2
  409. scitex/io/_save_modules/_yaml.py +9 -9
  410. scitex/io/_save_modules/_zarr.py +15 -15
  411. scitex/io/utils/__init__.py +2 -1
  412. scitex/io/utils/h5_to_zarr.py +183 -163
  413. scitex/linalg/__init__.py +1 -1
  414. scitex/linalg/_geometric_median.py +4 -3
  415. scitex/logging/_Tee.py +5 -7
  416. scitex/logging/__init__.py +18 -19
  417. scitex/logging/_config.py +4 -1
  418. scitex/logging/_context.py +6 -5
  419. scitex/logging/_formatters.py +2 -3
  420. scitex/logging/_handlers.py +19 -20
  421. scitex/logging/_levels.py +9 -17
  422. scitex/logging/_logger.py +74 -15
  423. scitex/logging/_print_capture.py +17 -17
  424. scitex/msword/__init__.py +255 -0
  425. scitex/msword/profiles.py +357 -0
  426. scitex/msword/reader.py +753 -0
  427. scitex/msword/utils.py +289 -0
  428. scitex/msword/writer.py +362 -0
  429. scitex/nn/_BNet.py +1 -3
  430. scitex/nn/_Filters.py +6 -2
  431. scitex/nn/_ModulationIndex.py +3 -1
  432. scitex/nn/_PAC.py +3 -2
  433. scitex/nn/_PSD.py +0 -1
  434. scitex/nn/__init__.py +16 -3
  435. scitex/path/_clean.py +10 -8
  436. scitex/path/_find.py +1 -1
  437. scitex/path/_get_spath.py +1 -2
  438. scitex/path/_mk_spath.py +1 -1
  439. scitex/path/_symlink.py +5 -10
  440. scitex/pd/__init__.py +4 -1
  441. scitex/pd/_force_df.py +24 -24
  442. scitex/pd/_get_unique.py +1 -0
  443. scitex/pd/_merge_columns.py +1 -1
  444. scitex/pd/_round.py +11 -7
  445. scitex/pd/_to_xy.py +0 -1
  446. scitex/plt/__init__.py +190 -89
  447. scitex/plt/_subplots/_AxesWrapper.py +28 -12
  448. scitex/plt/_subplots/_AxisWrapper.py +114 -47
  449. scitex/plt/_subplots/_AxisWrapperMixins/_AdjustmentMixin/__init__.py +36 -0
  450. scitex/plt/_subplots/_AxisWrapperMixins/_AdjustmentMixin/_labels.py +264 -0
  451. scitex/plt/_subplots/_AxisWrapperMixins/_AdjustmentMixin/_metadata.py +213 -0
  452. scitex/plt/_subplots/_AxisWrapperMixins/_AdjustmentMixin/_visual.py +128 -0
  453. scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin/__init__.py +59 -0
  454. scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin/_base.py +34 -0
  455. scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin/_scientific.py +593 -0
  456. scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin/_statistical.py +654 -0
  457. scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin/_stx_aliases.py +527 -0
  458. scitex/plt/_subplots/_AxisWrapperMixins/_RawMatplotlibMixin.py +321 -0
  459. scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin/__init__.py +33 -0
  460. scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin/_base.py +152 -0
  461. scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin/_wrappers.py +600 -0
  462. scitex/plt/_subplots/_AxisWrapperMixins/_TrackingMixin.py +26 -14
  463. scitex/plt/_subplots/_AxisWrapperMixins/_UnitAwareMixin.py +80 -73
  464. scitex/plt/_subplots/_AxisWrapperMixins/__init__.py +79 -5
  465. scitex/plt/_subplots/_FigWrapper.py +97 -64
  466. scitex/plt/_subplots/_SubplotsWrapper.py +161 -84
  467. scitex/plt/_subplots/__init__.py +10 -0
  468. scitex/plt/_subplots/_export_as_csv.py +124 -52
  469. scitex/plt/_subplots/_export_as_csv_formatters/__init__.py +9 -0
  470. scitex/plt/_subplots/_export_as_csv_formatters/_format_annotate.py +14 -23
  471. scitex/plt/_subplots/_export_as_csv_formatters/_format_bar.py +88 -38
  472. scitex/plt/_subplots/_export_as_csv_formatters/_format_barh.py +25 -31
  473. scitex/plt/_subplots/_export_as_csv_formatters/_format_boxplot.py +53 -23
  474. scitex/plt/_subplots/_export_as_csv_formatters/_format_contour.py +38 -25
  475. scitex/plt/_subplots/_export_as_csv_formatters/_format_contourf.py +17 -9
  476. scitex/plt/_subplots/_export_as_csv_formatters/_format_errorbar.py +70 -124
  477. scitex/plt/_subplots/_export_as_csv_formatters/_format_eventplot.py +30 -17
  478. scitex/plt/_subplots/_export_as_csv_formatters/_format_fill.py +31 -17
  479. scitex/plt/_subplots/_export_as_csv_formatters/_format_fill_between.py +33 -21
  480. scitex/plt/_subplots/_export_as_csv_formatters/_format_hexbin.py +14 -4
  481. scitex/plt/_subplots/_export_as_csv_formatters/_format_hist.py +43 -29
  482. scitex/plt/_subplots/_export_as_csv_formatters/_format_hist2d.py +14 -4
  483. scitex/plt/_subplots/_export_as_csv_formatters/_format_imshow.py +27 -11
  484. scitex/plt/_subplots/_export_as_csv_formatters/_format_imshow2d.py +34 -16
  485. scitex/plt/_subplots/_export_as_csv_formatters/_format_matshow.py +16 -8
  486. scitex/plt/_subplots/_export_as_csv_formatters/_format_pie.py +15 -6
  487. scitex/plt/_subplots/_export_as_csv_formatters/_format_plot.py +85 -46
  488. scitex/plt/_subplots/_export_as_csv_formatters/_format_plot_box.py +52 -27
  489. scitex/plt/_subplots/_export_as_csv_formatters/_format_plot_imshow.py +14 -1
  490. scitex/plt/_subplots/_export_as_csv_formatters/_format_plot_kde.py +27 -18
  491. scitex/plt/_subplots/_export_as_csv_formatters/_format_plot_scatter.py +14 -5
  492. scitex/plt/_subplots/_export_as_csv_formatters/_format_quiver.py +16 -8
  493. scitex/plt/_subplots/_export_as_csv_formatters/_format_scatter.py +17 -6
  494. scitex/plt/_subplots/_export_as_csv_formatters/_format_sns_barplot.py +43 -26
  495. scitex/plt/_subplots/_export_as_csv_formatters/_format_sns_boxplot.py +68 -47
  496. scitex/plt/_subplots/_export_as_csv_formatters/_format_sns_heatmap.py +52 -64
  497. scitex/plt/_subplots/_export_as_csv_formatters/_format_sns_histplot.py +55 -50
  498. scitex/plt/_subplots/_export_as_csv_formatters/_format_sns_jointplot.py +23 -10
  499. scitex/plt/_subplots/_export_as_csv_formatters/_format_sns_kdeplot.py +63 -29
  500. scitex/plt/_subplots/_export_as_csv_formatters/_format_sns_lineplot.py +48 -40
  501. scitex/plt/_subplots/_export_as_csv_formatters/_format_sns_pairplot.py +20 -6
  502. scitex/plt/_subplots/_export_as_csv_formatters/_format_sns_scatterplot.py +44 -40
  503. scitex/plt/_subplots/_export_as_csv_formatters/_format_sns_stripplot.py +46 -39
  504. scitex/plt/_subplots/_export_as_csv_formatters/_format_sns_swarmplot.py +46 -39
  505. scitex/plt/_subplots/_export_as_csv_formatters/_format_sns_violinplot.py +75 -94
  506. scitex/plt/_subplots/_export_as_csv_formatters/_format_stem.py +12 -3
  507. scitex/plt/_subplots/_export_as_csv_formatters/_format_step.py +12 -3
  508. scitex/plt/_subplots/_export_as_csv_formatters/_format_streamplot.py +17 -9
  509. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_bar.py +84 -0
  510. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_barh.py +85 -0
  511. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_conf_mat.py +31 -18
  512. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_contour.py +54 -0
  513. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_ecdf.py +24 -11
  514. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_errorbar.py +120 -0
  515. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_fillv.py +35 -31
  516. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_heatmap.py +33 -23
  517. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_image.py +44 -28
  518. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_imshow.py +63 -0
  519. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_joyplot.py +31 -12
  520. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_line.py +34 -23
  521. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_mean_ci.py +32 -26
  522. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_mean_std.py +29 -23
  523. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_median_iqr.py +32 -26
  524. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_raster.py +21 -11
  525. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_rectangle.py +84 -56
  526. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_scatter.py +51 -0
  527. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_scatter_hist.py +46 -34
  528. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_shaded_line.py +46 -30
  529. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_violin.py +51 -51
  530. scitex/plt/_subplots/_export_as_csv_formatters/_format_text.py +32 -31
  531. scitex/plt/_subplots/_export_as_csv_formatters/_format_violin.py +34 -31
  532. scitex/plt/_subplots/_export_as_csv_formatters/_format_violinplot.py +44 -37
  533. scitex/plt/_subplots/_export_as_csv_formatters/verify_formatters.py +91 -74
  534. scitex/plt/_tpl.py +6 -5
  535. scitex/plt/ax/_plot/__init__.py +24 -0
  536. scitex/plt/ax/_plot/_add_fitted_line.py +12 -11
  537. scitex/plt/ax/_plot/_plot_circular_hist.py +3 -1
  538. scitex/plt/ax/_plot/_plot_statistical_shaded_line.py +25 -19
  539. scitex/plt/ax/_plot/_stx_conf_mat.py +6 -3
  540. scitex/plt/ax/_plot/_stx_ecdf.py +9 -5
  541. scitex/plt/ax/_plot/_stx_fillv.py +4 -2
  542. scitex/plt/ax/_plot/_stx_heatmap.py +7 -4
  543. scitex/plt/ax/_plot/_stx_image.py +7 -5
  544. scitex/plt/ax/_plot/_stx_joyplot.py +32 -10
  545. scitex/plt/ax/_plot/_stx_raster.py +26 -11
  546. scitex/plt/ax/_plot/_stx_rectangle.py +2 -2
  547. scitex/plt/ax/_plot/_stx_shaded_line.py +15 -11
  548. scitex/plt/ax/_plot/_stx_violin.py +3 -1
  549. scitex/plt/ax/_style/_add_marginal_ax.py +6 -4
  550. scitex/plt/ax/_style/_auto_scale_axis.py +14 -10
  551. scitex/plt/ax/_style/_extend.py +3 -1
  552. scitex/plt/ax/_style/_force_aspect.py +5 -3
  553. scitex/plt/ax/_style/_format_units.py +2 -2
  554. scitex/plt/ax/_style/_hide_spines.py +5 -1
  555. scitex/plt/ax/_style/_map_ticks.py +5 -3
  556. scitex/plt/ax/_style/_rotate_labels.py +5 -4
  557. scitex/plt/ax/_style/_rotate_labels_v01.py +73 -63
  558. scitex/plt/ax/_style/_set_log_scale.py +120 -85
  559. scitex/plt/ax/_style/_set_meta.py +99 -76
  560. scitex/plt/ax/_style/_set_supxyt.py +33 -16
  561. scitex/plt/ax/_style/_set_xyt.py +27 -18
  562. scitex/plt/ax/_style/_share_axes.py +15 -5
  563. scitex/plt/ax/_style/_show_spines.py +58 -57
  564. scitex/plt/ax/_style/_style_barplot.py +1 -1
  565. scitex/plt/ax/_style/_style_boxplot.py +25 -14
  566. scitex/plt/ax/_style/_style_errorbar.py +0 -0
  567. scitex/plt/ax/_style/_style_scatter.py +1 -1
  568. scitex/plt/ax/_style/_style_suptitles.py +3 -3
  569. scitex/plt/ax/_style/_style_violinplot.py +8 -2
  570. scitex/plt/color/__init__.py +34 -2
  571. scitex/plt/color/_add_hue_col.py +1 -0
  572. scitex/plt/color/_colors.py +0 -1
  573. scitex/plt/color/_get_colors_from_conf_matap.py +3 -1
  574. scitex/plt/color/_vizualize_colors.py +0 -1
  575. scitex/plt/docs/FIGURE_ARCHITECTURE.md +155 -97
  576. scitex/plt/gallery/README.md +75 -0
  577. scitex/plt/gallery/__init__.py +29 -0
  578. scitex/plt/gallery/_generate.py +560 -0
  579. scitex/plt/gallery/_plots.py +594 -0
  580. scitex/plt/gallery/_registry.py +153 -0
  581. scitex/plt/io/__init__.py +53 -0
  582. scitex/plt/io/_bundle.py +490 -0
  583. scitex/plt/io/_layered_bundle.py +1343 -0
  584. scitex/plt/styles/SCITEX_STYLE.yaml +26 -0
  585. scitex/plt/styles/__init__.py +23 -9
  586. scitex/plt/styles/_plot_defaults.py +62 -61
  587. scitex/plt/styles/_plot_postprocess.py +126 -77
  588. scitex/plt/styles/_style_loader.py +0 -0
  589. scitex/plt/styles/presets.py +121 -18
  590. scitex/plt/utils/__init__.py +42 -3
  591. scitex/plt/utils/_close.py +8 -3
  592. scitex/plt/utils/_collect_figure_metadata.py +3033 -271
  593. scitex/plt/utils/_colorbar.py +15 -17
  594. scitex/plt/utils/_configure_mpl.py +26 -30
  595. scitex/plt/utils/_crop.py +87 -36
  596. scitex/plt/utils/_csv_column_naming.py +177 -72
  597. scitex/plt/utils/_dimension_viewer.py +7 -19
  598. scitex/plt/utils/_figure_from_axes_mm.py +70 -16
  599. scitex/plt/utils/_figure_mm.py +119 -3
  600. scitex/plt/utils/_get_actual_font.py +5 -4
  601. scitex/plt/utils/_histogram_utils.py +52 -48
  602. scitex/plt/utils/_hitmap.py +1643 -0
  603. scitex/plt/utils/_is_valid_axis.py +19 -13
  604. scitex/plt/utils/_mk_colorbar.py +3 -3
  605. scitex/plt/utils/_scientific_captions.py +202 -139
  606. scitex/plt/utils/_scitex_config.py +98 -98
  607. scitex/plt/utils/_units.py +0 -0
  608. scitex/plt/utils/metadata/__init__.py +61 -0
  609. scitex/plt/utils/metadata/_artist_extraction.py +119 -0
  610. scitex/plt/utils/metadata/_axes_metadata.py +93 -0
  611. scitex/plt/utils/metadata/_collection_artists.py +292 -0
  612. scitex/plt/utils/metadata/_core.py +207 -0
  613. scitex/plt/utils/metadata/_csv_column_extraction.py +186 -0
  614. scitex/plt/utils/metadata/_csv_hash.py +115 -0
  615. scitex/plt/utils/metadata/_csv_verification.py +95 -0
  616. scitex/plt/utils/metadata/_data_linkage.py +263 -0
  617. scitex/plt/utils/metadata/_dimensions.py +242 -0
  618. scitex/plt/utils/metadata/_editable_export.py +405 -0
  619. scitex/plt/utils/metadata/_figure_metadata.py +58 -0
  620. scitex/plt/utils/metadata/_geometry_extraction.py +570 -0
  621. scitex/plt/utils/metadata/_image_text_artists.py +168 -0
  622. scitex/plt/utils/metadata/_label_parsing.py +82 -0
  623. scitex/plt/utils/metadata/_legend_extraction.py +120 -0
  624. scitex/plt/utils/metadata/_line_artists.py +367 -0
  625. scitex/plt/utils/metadata/_line_semantic_handling.py +173 -0
  626. scitex/plt/utils/metadata/_patch_artists.py +211 -0
  627. scitex/plt/utils/metadata/_plot_content.py +26 -0
  628. scitex/plt/utils/metadata/_plot_type_detection.py +184 -0
  629. scitex/plt/utils/metadata/_precision.py +134 -0
  630. scitex/plt/utils/metadata/_precision_config.py +68 -0
  631. scitex/plt/utils/metadata/_precision_sections.py +211 -0
  632. scitex/plt/utils/metadata/_recipe_extraction.py +267 -0
  633. scitex/plt/utils/metadata/_style_parsing.py +174 -0
  634. scitex/repro/_RandomStateManager.py +33 -38
  635. scitex/repro/__init__.py +16 -7
  636. scitex/repro/_gen_ID.py +7 -9
  637. scitex/repro/_gen_timestamp.py +7 -6
  638. scitex/repro/_hash_array.py +8 -12
  639. scitex/reproduce/__init__.py +1 -1
  640. scitex/resource/_get_processor_usages.py +3 -1
  641. scitex/resource/_log_processor_usages.py +3 -1
  642. scitex/rng/__init__.py +1 -1
  643. scitex/schema/README.md +178 -0
  644. scitex/schema/__init__.py +237 -0
  645. scitex/schema/_canvas.py +444 -0
  646. scitex/schema/_plot.py +1015 -0
  647. scitex/schema/_stats.py +762 -0
  648. scitex/schema/_validation.py +590 -0
  649. scitex/scholar/.legacy/Scholar.py +5 -12
  650. scitex/scholar/.legacy/_Scholar.py +66 -99
  651. scitex/scholar/.legacy/_ScholarAPI.py +75 -66
  652. scitex/scholar/.legacy/_tmp/search_engine/_BaseSearchEngine.py +3 -3
  653. scitex/scholar/.legacy/_tmp/search_engine/_UnifiedSearcher.py +4 -9
  654. scitex/scholar/.legacy/_tmp/search_engine/__init__.py +14 -21
  655. scitex/scholar/.legacy/_tmp/search_engine/local/_LocalSearchEngine.py +40 -37
  656. scitex/scholar/.legacy/_tmp/search_engine/local/_VectorSearchEngine.py +31 -28
  657. scitex/scholar/.legacy/_tmp/search_engine/web/_ArxivSearchEngine.py +74 -65
  658. scitex/scholar/.legacy/_tmp/search_engine/web/_CrossRefSearchEngine.py +122 -116
  659. scitex/scholar/.legacy/_tmp/search_engine/web/_GoogleScholarSearchEngine.py +65 -59
  660. scitex/scholar/.legacy/_tmp/search_engine/web/_PubMedSearchEngine.py +121 -107
  661. scitex/scholar/.legacy/_tmp/search_engine/web/_SemanticScholarSearchEngine.py +5 -12
  662. scitex/scholar/.legacy/database/_DatabaseEntry.py +49 -45
  663. scitex/scholar/.legacy/database/_DatabaseIndex.py +131 -94
  664. scitex/scholar/.legacy/database/_LibraryManager.py +65 -63
  665. scitex/scholar/.legacy/database/_PaperDatabase.py +138 -124
  666. scitex/scholar/.legacy/database/_ScholarDatabaseIntegration.py +14 -36
  667. scitex/scholar/.legacy/database/_StorageIntegratedDB.py +192 -156
  668. scitex/scholar/.legacy/database/_ZoteroCompatibleDB.py +300 -237
  669. scitex/scholar/.legacy/database/__init__.py +2 -1
  670. scitex/scholar/.legacy/database/manage.py +92 -84
  671. scitex/scholar/.legacy/lookup/_LookupIndex.py +157 -101
  672. scitex/scholar/.legacy/lookup/__init__.py +2 -1
  673. scitex/scholar/.legacy/metadata/doi/batch/_MetadataHandlerForBatchDOIResolution.py +4 -9
  674. scitex/scholar/.legacy/metadata/doi/batch/_ProgressManagerForBatchDOIResolution.py +10 -23
  675. scitex/scholar/.legacy/metadata/doi/batch/_SourceStatsManagerForBatchDOIResolution.py +4 -9
  676. scitex/scholar/.legacy/metadata/doi/batch/__init__.py +3 -1
  677. scitex/scholar/.legacy/metadata/doi/resolvers/_BatchDOIResolver.py +10 -25
  678. scitex/scholar/.legacy/metadata/doi/resolvers/_BibTeXDOIResolver.py +19 -49
  679. scitex/scholar/.legacy/metadata/doi/resolvers/_DOIResolver.py +1 -0
  680. scitex/scholar/.legacy/metadata/doi/resolvers/_SingleDOIResolver.py +8 -20
  681. scitex/scholar/.legacy/metadata/doi/sources/.combined-SemanticScholarSource/_SemanticScholarSource.py +37 -35
  682. scitex/scholar/.legacy/metadata/doi/sources/.combined-SemanticScholarSource/_SemanticScholarSourceEnhanced.py +49 -37
  683. scitex/scholar/.legacy/metadata/doi/sources/_ArXivSource.py +11 -30
  684. scitex/scholar/.legacy/metadata/doi/sources/_BaseDOISource.py +19 -47
  685. scitex/scholar/.legacy/metadata/doi/sources/_CrossRefLocalSource.py +1 -0
  686. scitex/scholar/.legacy/metadata/doi/sources/_CrossRefSource.py +12 -33
  687. scitex/scholar/.legacy/metadata/doi/sources/_OpenAlexSource.py +8 -20
  688. scitex/scholar/.legacy/metadata/doi/sources/_PubMedSource.py +10 -27
  689. scitex/scholar/.legacy/metadata/doi/sources/_SemanticScholarSource.py +11 -29
  690. scitex/scholar/.legacy/metadata/doi/sources/_SourceManager.py +8 -21
  691. scitex/scholar/.legacy/metadata/doi/sources/_SourceResolutionStrategy.py +24 -55
  692. scitex/scholar/.legacy/metadata/doi/sources/_SourceRotationManager.py +8 -21
  693. scitex/scholar/.legacy/metadata/doi/sources/_URLDOISource.py +9 -16
  694. scitex/scholar/.legacy/metadata/doi/sources/_UnifiedSource.py +8 -22
  695. scitex/scholar/.legacy/metadata/doi/sources/__init__.py +1 -0
  696. scitex/scholar/.legacy/metadata/doi/utils/_PubMedConverter.py +4 -8
  697. scitex/scholar/.legacy/metadata/doi/utils/_RateLimitHandler.py +17 -43
  698. scitex/scholar/.legacy/metadata/doi/utils/_TextNormalizer.py +8 -18
  699. scitex/scholar/.legacy/metadata/doi/utils/_URLDOIExtractor.py +4 -8
  700. scitex/scholar/.legacy/metadata/doi/utils/__init__.py +1 -0
  701. scitex/scholar/.legacy/metadata/doi/utils/_to_complete_metadata_structure.py +1 -0
  702. scitex/scholar/.legacy/metadata/enrichment/_LibraryEnricher.py +2 -3
  703. scitex/scholar/.legacy/metadata/enrichment/enrichers/_ImpactFactorEnricher.py +6 -12
  704. scitex/scholar/.legacy/metadata/enrichment/enrichers/_SmartEnricher.py +5 -10
  705. scitex/scholar/.legacy/metadata/enrichment/sources/_UnifiedMetadataSource.py +4 -5
  706. scitex/scholar/.legacy/metadata/query_to_full_meta_json.py +8 -12
  707. scitex/scholar/.legacy/metadata/urls/_URLMetadataHandler.py +3 -3
  708. scitex/scholar/.legacy/metadata/urls/_ZoteroTranslatorRunner.py +15 -21
  709. scitex/scholar/.legacy/metadata/urls/__init__.py +3 -3
  710. scitex/scholar/.legacy/metadata/urls/_finder.py +4 -6
  711. scitex/scholar/.legacy/metadata/urls/_handler.py +7 -15
  712. scitex/scholar/.legacy/metadata/urls/_resolver.py +6 -12
  713. scitex/scholar/.legacy/search/_Embedder.py +74 -69
  714. scitex/scholar/.legacy/search/_SemanticSearch.py +91 -90
  715. scitex/scholar/.legacy/search/_SemanticSearchEngine.py +104 -109
  716. scitex/scholar/.legacy/search/_UnifiedSearcher.py +530 -471
  717. scitex/scholar/.legacy/search/_VectorDatabase.py +111 -92
  718. scitex/scholar/.legacy/search/__init__.py +1 -0
  719. scitex/scholar/.legacy/storage/_EnhancedStorageManager.py +182 -154
  720. scitex/scholar/.legacy/storage/__init__.py +2 -1
  721. scitex/scholar/__init__.py +0 -2
  722. scitex/scholar/__main__.py +1 -3
  723. scitex/scholar/auth/ScholarAuthManager.py +13 -36
  724. scitex/scholar/auth/core/AuthenticationGateway.py +15 -29
  725. scitex/scholar/auth/core/BrowserAuthenticator.py +22 -57
  726. scitex/scholar/auth/core/StrategyResolver.py +10 -27
  727. scitex/scholar/auth/core/__init__.py +5 -1
  728. scitex/scholar/auth/gateway/_OpenURLLinkFinder.py +11 -21
  729. scitex/scholar/auth/gateway/_OpenURLResolver.py +10 -18
  730. scitex/scholar/auth/gateway/_resolve_functions.py +3 -3
  731. scitex/scholar/auth/providers/BaseAuthenticator.py +1 -0
  732. scitex/scholar/auth/providers/EZProxyAuthenticator.py +7 -14
  733. scitex/scholar/auth/providers/OpenAthensAuthenticator.py +29 -57
  734. scitex/scholar/auth/providers/ShibbolethAuthenticator.py +87 -73
  735. scitex/scholar/auth/session/AuthCacheManager.py +12 -22
  736. scitex/scholar/auth/session/SessionManager.py +4 -6
  737. scitex/scholar/auth/sso/BaseSSOAutomator.py +13 -19
  738. scitex/scholar/auth/sso/OpenAthensSSOAutomator.py +16 -45
  739. scitex/scholar/auth/sso/SSOAutomator.py +8 -15
  740. scitex/scholar/auth/sso/UniversityOfMelbourneSSOAutomator.py +13 -23
  741. scitex/scholar/browser/ScholarBrowserManager.py +31 -56
  742. scitex/scholar/browser/__init__.py +1 -0
  743. scitex/scholar/browser/utils/click_and_wait.py +3 -4
  744. scitex/scholar/browser/utils/close_unwanted_pages.py +4 -7
  745. scitex/scholar/browser/utils/wait_redirects.py +15 -40
  746. scitex/scholar/citation_graph/__init__.py +0 -0
  747. scitex/scholar/citation_graph/builder.py +3 -7
  748. scitex/scholar/citation_graph/database.py +4 -11
  749. scitex/scholar/citation_graph/example.py +5 -10
  750. scitex/scholar/citation_graph/models.py +0 -0
  751. scitex/scholar/cli/_url_utils.py +1 -1
  752. scitex/scholar/cli/chrome.py +5 -3
  753. scitex/scholar/cli/download_pdf.py +13 -14
  754. scitex/scholar/cli/handlers/bibtex_handler.py +4 -12
  755. scitex/scholar/cli/handlers/doi_handler.py +1 -3
  756. scitex/scholar/cli/handlers/project_handler.py +6 -20
  757. scitex/scholar/cli/open_browser.py +41 -39
  758. scitex/scholar/cli/open_browser_auto.py +31 -39
  759. scitex/scholar/cli/open_browser_monitored.py +27 -24
  760. scitex/scholar/config/ScholarConfig.py +5 -8
  761. scitex/scholar/config/__init__.py +1 -0
  762. scitex/scholar/config/core/_CascadeConfig.py +3 -3
  763. scitex/scholar/config/core/_PathManager.py +16 -28
  764. scitex/scholar/core/Paper.py +79 -78
  765. scitex/scholar/core/Papers.py +16 -27
  766. scitex/scholar/core/Scholar.py +98 -229
  767. scitex/scholar/core/journal_normalizer.py +52 -49
  768. scitex/scholar/core/oa_cache.py +27 -23
  769. scitex/scholar/core/open_access.py +17 -8
  770. scitex/scholar/docs/template.py +4 -3
  771. scitex/scholar/docs/to_claude/examples/example-python-project-scitex/scripts/mnist/clf_svm.py +0 -0
  772. scitex/scholar/docs/to_claude/examples/example-python-project-scitex/scripts/mnist/download.py +0 -0
  773. scitex/scholar/docs/to_claude/examples/example-python-project-scitex/scripts/mnist/plot_conf_mat.py +0 -0
  774. scitex/scholar/docs/to_claude/examples/example-python-project-scitex/scripts/mnist/plot_digits.py +0 -0
  775. scitex/scholar/docs/to_claude/examples/example-python-project-scitex/scripts/mnist/plot_umap_space.py +0 -0
  776. scitex/scholar/examples/00_config.py +10 -9
  777. scitex/scholar/examples/01_auth.py +3 -0
  778. scitex/scholar/examples/02_browser.py +14 -10
  779. scitex/scholar/examples/03_01-engine.py +3 -0
  780. scitex/scholar/examples/03_02-engine-for-bibtex.py +4 -3
  781. scitex/scholar/examples/04_01-url.py +9 -9
  782. scitex/scholar/examples/04_02-url-for-bibtex.py +7 -3
  783. scitex/scholar/examples/04_02-url-for-dois.py +87 -97
  784. scitex/scholar/examples/05_download_pdf.py +10 -4
  785. scitex/scholar/examples/06_find_and_download.py +6 -6
  786. scitex/scholar/examples/06_parse_bibtex.py +17 -17
  787. scitex/scholar/examples/07_storage_integration.py +6 -9
  788. scitex/scholar/examples/99_fullpipeline-for-bibtex.py +14 -15
  789. scitex/scholar/examples/99_fullpipeline-for-one-entry.py +31 -23
  790. scitex/scholar/examples/99_maintenance.py +3 -0
  791. scitex/scholar/examples/dev.py +2 -3
  792. scitex/scholar/examples/zotero_integration.py +11 -18
  793. scitex/scholar/impact_factor/ImpactFactorEngine.py +7 -9
  794. scitex/scholar/impact_factor/estimation/__init__.py +4 -4
  795. scitex/scholar/impact_factor/estimation/core/__init__.py +3 -7
  796. scitex/scholar/impact_factor/estimation/core/cache_manager.py +223 -211
  797. scitex/scholar/impact_factor/estimation/core/calculator.py +165 -131
  798. scitex/scholar/impact_factor/estimation/core/journal_matcher.py +217 -172
  799. scitex/scholar/impact_factor/jcr/ImpactFactorJCREngine.py +6 -14
  800. scitex/scholar/impact_factor/jcr/build_database.py +4 -3
  801. scitex/scholar/integration/base.py +9 -17
  802. scitex/scholar/integration/mendeley/exporter.py +2 -4
  803. scitex/scholar/integration/mendeley/importer.py +3 -3
  804. scitex/scholar/integration/mendeley/linker.py +3 -3
  805. scitex/scholar/integration/mendeley/mapper.py +9 -6
  806. scitex/scholar/integration/zotero/__main__.py +26 -43
  807. scitex/scholar/integration/zotero/exporter.py +15 -11
  808. scitex/scholar/integration/zotero/importer.py +12 -10
  809. scitex/scholar/integration/zotero/linker.py +8 -12
  810. scitex/scholar/integration/zotero/mapper.py +17 -12
  811. scitex/scholar/metadata_engines/.combined-SemanticScholarSource/_SemanticScholarSource.py +37 -35
  812. scitex/scholar/metadata_engines/.combined-SemanticScholarSource/_SemanticScholarSourceEnhanced.py +47 -35
  813. scitex/scholar/metadata_engines/ScholarEngine.py +21 -43
  814. scitex/scholar/metadata_engines/__init__.py +1 -0
  815. scitex/scholar/metadata_engines/individual/ArXivEngine.py +15 -37
  816. scitex/scholar/metadata_engines/individual/CrossRefEngine.py +15 -42
  817. scitex/scholar/metadata_engines/individual/CrossRefLocalEngine.py +24 -45
  818. scitex/scholar/metadata_engines/individual/OpenAlexEngine.py +11 -21
  819. scitex/scholar/metadata_engines/individual/PubMedEngine.py +10 -27
  820. scitex/scholar/metadata_engines/individual/SemanticScholarEngine.py +28 -35
  821. scitex/scholar/metadata_engines/individual/URLDOIEngine.py +11 -22
  822. scitex/scholar/metadata_engines/individual/_BaseDOIEngine.py +20 -49
  823. scitex/scholar/metadata_engines/utils/_PubMedConverter.py +4 -8
  824. scitex/scholar/metadata_engines/utils/_URLDOIExtractor.py +5 -10
  825. scitex/scholar/metadata_engines/utils/__init__.py +2 -0
  826. scitex/scholar/metadata_engines/utils/_metadata2bibtex.py +3 -0
  827. scitex/scholar/metadata_engines/utils/_standardize_metadata.py +2 -3
  828. scitex/scholar/pdf_download/ScholarPDFDownloader.py +25 -37
  829. scitex/scholar/pdf_download/strategies/chrome_pdf_viewer.py +11 -19
  830. scitex/scholar/pdf_download/strategies/direct_download.py +5 -9
  831. scitex/scholar/pdf_download/strategies/manual_download_fallback.py +3 -3
  832. scitex/scholar/pdf_download/strategies/manual_download_utils.py +6 -13
  833. scitex/scholar/pdf_download/strategies/open_access_download.py +49 -31
  834. scitex/scholar/pdf_download/strategies/response_body.py +8 -19
  835. scitex/scholar/pipelines/ScholarPipelineBibTeX.py +9 -18
  836. scitex/scholar/pipelines/ScholarPipelineMetadataParallel.py +25 -26
  837. scitex/scholar/pipelines/ScholarPipelineMetadataSingle.py +62 -23
  838. scitex/scholar/pipelines/ScholarPipelineParallel.py +13 -30
  839. scitex/scholar/pipelines/ScholarPipelineSearchParallel.py +299 -220
  840. scitex/scholar/pipelines/ScholarPipelineSearchSingle.py +202 -165
  841. scitex/scholar/pipelines/ScholarPipelineSingle.py +25 -51
  842. scitex/scholar/pipelines/SearchQueryParser.py +55 -55
  843. scitex/scholar/search_engines/ScholarSearchEngine.py +31 -27
  844. scitex/scholar/search_engines/_BaseSearchEngine.py +20 -23
  845. scitex/scholar/search_engines/individual/ArXivSearchEngine.py +53 -35
  846. scitex/scholar/search_engines/individual/CrossRefSearchEngine.py +47 -40
  847. scitex/scholar/search_engines/individual/OpenAlexSearchEngine.py +55 -50
  848. scitex/scholar/search_engines/individual/PubMedSearchEngine.py +8 -10
  849. scitex/scholar/search_engines/individual/SemanticScholarSearchEngine.py +55 -49
  850. scitex/scholar/storage/BibTeXHandler.py +150 -95
  851. scitex/scholar/storage/PaperIO.py +3 -6
  852. scitex/scholar/storage/ScholarLibrary.py +70 -49
  853. scitex/scholar/storage/_DeduplicationManager.py +52 -25
  854. scitex/scholar/storage/_LibraryCacheManager.py +19 -46
  855. scitex/scholar/storage/_LibraryManager.py +65 -175
  856. scitex/scholar/url_finder/ScholarURLFinder.py +9 -25
  857. scitex/scholar/url_finder/strategies/find_pdf_urls_by_direct_links.py +1 -1
  858. scitex/scholar/url_finder/strategies/find_pdf_urls_by_href.py +6 -10
  859. scitex/scholar/url_finder/strategies/find_pdf_urls_by_navigation.py +4 -6
  860. scitex/scholar/url_finder/strategies/find_pdf_urls_by_publisher_patterns.py +8 -15
  861. scitex/scholar/url_finder/strategies/find_pdf_urls_by_zotero_translators.py +3 -3
  862. scitex/scholar/url_finder/strategies/find_supplementary_urls_by_href.py +3 -3
  863. scitex/scholar/url_finder/translators/core/patterns.py +6 -4
  864. scitex/scholar/url_finder/translators/core/registry.py +6 -9
  865. scitex/scholar/url_finder/translators/individual/BOFiP_Impots.py +60 -52
  866. scitex/scholar/url_finder/translators/individual/Baidu_Scholar.py +54 -62
  867. scitex/scholar/url_finder/translators/individual/Bangkok_Post.py +38 -44
  868. scitex/scholar/url_finder/translators/individual/Baruch_Foundation.py +43 -47
  869. scitex/scholar/url_finder/translators/individual/Beobachter.py +46 -50
  870. scitex/scholar/url_finder/translators/individual/Bezneng_Gajit.py +37 -41
  871. scitex/scholar/url_finder/translators/individual/BibLaTeX.py +59 -52
  872. scitex/scholar/url_finder/translators/individual/BibTeX.py +83 -79
  873. scitex/scholar/url_finder/translators/individual/Biblio_com.py +48 -51
  874. scitex/scholar/url_finder/translators/individual/Bibliontology_RDF.py +58 -56
  875. scitex/scholar/url_finder/translators/individual/Camara_Brasileira_do_Livro_ISBN.py +102 -99
  876. scitex/scholar/url_finder/translators/individual/CanLII.py +49 -43
  877. scitex/scholar/url_finder/translators/individual/Canada_com.py +36 -40
  878. scitex/scholar/url_finder/translators/individual/Canadian_Letters_and_Images.py +43 -43
  879. scitex/scholar/url_finder/translators/individual/Canadiana_ca.py +77 -66
  880. scitex/scholar/url_finder/translators/individual/Cascadilla_Proceedings_Project.py +68 -62
  881. scitex/scholar/url_finder/translators/individual/Central_and_Eastern_European_Online_Library_Journals.py +60 -60
  882. scitex/scholar/url_finder/translators/individual/Champlain_Society_Collection.py +63 -61
  883. scitex/scholar/url_finder/translators/individual/Chicago_Journal_of_Theoretical_Computer_Science.py +74 -58
  884. scitex/scholar/url_finder/translators/individual/Christian_Science_Monitor.py +32 -38
  885. scitex/scholar/url_finder/translators/individual/Columbia_University_Press.py +51 -47
  886. scitex/scholar/url_finder/translators/individual/Common_Place.py +66 -57
  887. scitex/scholar/url_finder/translators/individual/Cornell_LII.py +66 -62
  888. scitex/scholar/url_finder/translators/individual/Cornell_University_Press.py +38 -45
  889. scitex/scholar/url_finder/translators/individual/CourtListener.py +52 -56
  890. scitex/scholar/url_finder/translators/individual/DAI_Zenon.py +53 -54
  891. scitex/scholar/url_finder/translators/individual/access_medicine.py +27 -33
  892. scitex/scholar/url_finder/translators/individual/acm.py +1 -1
  893. scitex/scholar/url_finder/translators/individual/acm_digital_library.py +93 -63
  894. scitex/scholar/url_finder/translators/individual/airiti.py +3 -1
  895. scitex/scholar/url_finder/translators/individual/aosic.py +3 -1
  896. scitex/scholar/url_finder/translators/individual/archive_ouverte_aosic.py +3 -1
  897. scitex/scholar/url_finder/translators/individual/archive_ouverte_en_sciences_de_l_information_et_de_la_communication___aosic_.py +6 -2
  898. scitex/scholar/url_finder/translators/individual/artforum.py +35 -27
  899. scitex/scholar/url_finder/translators/individual/arxiv.py +1 -1
  900. scitex/scholar/url_finder/translators/individual/arxiv_org.py +8 -4
  901. scitex/scholar/url_finder/translators/individual/atlanta_journal_constitution.py +22 -18
  902. scitex/scholar/url_finder/translators/individual/atypon_journals.py +19 -11
  903. scitex/scholar/url_finder/translators/individual/austlii_and_nzlii.py +48 -44
  904. scitex/scholar/url_finder/translators/individual/australian_dictionary_of_biography.py +21 -17
  905. scitex/scholar/url_finder/translators/individual/bailii.py +22 -19
  906. scitex/scholar/url_finder/translators/individual/bbc.py +46 -42
  907. scitex/scholar/url_finder/translators/individual/bbc_genome.py +37 -25
  908. scitex/scholar/url_finder/translators/individual/biblioteca_nacional_de_maestros.py +24 -20
  909. scitex/scholar/url_finder/translators/individual/bibliotheque_archives_nationale_quebec_pistard.py +42 -43
  910. scitex/scholar/url_finder/translators/individual/bibliotheque_archives_nationales_quebec.py +87 -81
  911. scitex/scholar/url_finder/translators/individual/bibliotheque_nationale_france.py +39 -37
  912. scitex/scholar/url_finder/translators/individual/bibsys.py +32 -28
  913. scitex/scholar/url_finder/translators/individual/bioconductor.py +58 -52
  914. scitex/scholar/url_finder/translators/individual/biomed_central.py +23 -15
  915. scitex/scholar/url_finder/translators/individual/biorxiv.py +26 -13
  916. scitex/scholar/url_finder/translators/individual/blogger.py +39 -43
  917. scitex/scholar/url_finder/translators/individual/bloomberg.py +48 -52
  918. scitex/scholar/url_finder/translators/individual/bloomsbury_food_library.py +37 -37
  919. scitex/scholar/url_finder/translators/individual/bluesky.py +30 -28
  920. scitex/scholar/url_finder/translators/individual/bnf_isbn.py +1 -1
  921. scitex/scholar/url_finder/translators/individual/bocc.py +66 -60
  922. scitex/scholar/url_finder/translators/individual/boe.py +52 -52
  923. scitex/scholar/url_finder/translators/individual/brill.py +3 -1
  924. scitex/scholar/url_finder/translators/individual/business_standard.py +36 -38
  925. scitex/scholar/url_finder/translators/individual/cabi_cab_abstracts.py +39 -41
  926. scitex/scholar/url_finder/translators/individual/cambridge.py +3 -1
  927. scitex/scholar/url_finder/translators/individual/cambridge_core.py +30 -24
  928. scitex/scholar/url_finder/translators/individual/caod.py +50 -46
  929. scitex/scholar/url_finder/translators/individual/cbc.py +91 -67
  930. scitex/scholar/url_finder/translators/individual/ccfr_bnf.py +49 -53
  931. scitex/scholar/url_finder/translators/individual/cia_world_factbook.py +43 -33
  932. scitex/scholar/url_finder/translators/individual/crossref_rest.py +208 -174
  933. scitex/scholar/url_finder/translators/individual/current_affairs.py +29 -35
  934. scitex/scholar/url_finder/translators/individual/dabi.py +70 -66
  935. scitex/scholar/url_finder/translators/individual/dagens_nyheter.py +3 -1
  936. scitex/scholar/url_finder/translators/individual/dagstuhl.py +10 -15
  937. scitex/scholar/url_finder/translators/individual/dar_almandumah.py +13 -9
  938. scitex/scholar/url_finder/translators/individual/dart_europe.py +19 -22
  939. scitex/scholar/url_finder/translators/individual/data_gov.py +2 -2
  940. scitex/scholar/url_finder/translators/individual/databrary.py +27 -28
  941. scitex/scholar/url_finder/translators/individual/datacite_json.py +152 -137
  942. scitex/scholar/url_finder/translators/individual/dataverse.py +68 -64
  943. scitex/scholar/url_finder/translators/individual/daum_news.py +38 -38
  944. scitex/scholar/url_finder/translators/individual/dblp.py +4 -8
  945. scitex/scholar/url_finder/translators/individual/dblp_computer_science_bibliography.py +8 -3
  946. scitex/scholar/url_finder/translators/individual/dbpia.py +5 -3
  947. scitex/scholar/url_finder/translators/individual/defense_technical_information_center.py +30 -28
  948. scitex/scholar/url_finder/translators/individual/delpher.py +102 -79
  949. scitex/scholar/url_finder/translators/individual/demographic_research.py +35 -31
  950. scitex/scholar/url_finder/translators/individual/denik_cz.py +58 -54
  951. scitex/scholar/url_finder/translators/individual/depatisnet.py +7 -10
  952. scitex/scholar/url_finder/translators/individual/der_freitag.py +81 -66
  953. scitex/scholar/url_finder/translators/individual/der_spiegel.py +56 -54
  954. scitex/scholar/url_finder/translators/individual/digibib_net.py +3 -1
  955. scitex/scholar/url_finder/translators/individual/digizeitschriften.py +3 -1
  956. scitex/scholar/url_finder/translators/individual/dpla.py +13 -14
  957. scitex/scholar/url_finder/translators/individual/dspace.py +2 -2
  958. scitex/scholar/url_finder/translators/individual/ebrary.py +3 -1
  959. scitex/scholar/url_finder/translators/individual/ebscohost.py +3 -1
  960. scitex/scholar/url_finder/translators/individual/electronic_colloquium_on_computational_complexity.py +3 -1
  961. scitex/scholar/url_finder/translators/individual/elife.py +3 -1
  962. scitex/scholar/url_finder/translators/individual/elsevier_health_journals.py +3 -1
  963. scitex/scholar/url_finder/translators/individual/emerald.py +3 -1
  964. scitex/scholar/url_finder/translators/individual/emerald_insight.py +3 -1
  965. scitex/scholar/url_finder/translators/individual/epicurious.py +3 -1
  966. scitex/scholar/url_finder/translators/individual/eurogamerusgamer.py +3 -1
  967. scitex/scholar/url_finder/translators/individual/fachportal_padagogik.py +3 -1
  968. scitex/scholar/url_finder/translators/individual/frontiers.py +1 -1
  969. scitex/scholar/url_finder/translators/individual/gale_databases.py +3 -1
  970. scitex/scholar/url_finder/translators/individual/gms_german_medical_science.py +6 -2
  971. scitex/scholar/url_finder/translators/individual/ieee_computer_society.py +6 -2
  972. scitex/scholar/url_finder/translators/individual/ieee_xplore.py +41 -35
  973. scitex/scholar/url_finder/translators/individual/inter_research_science_center.py +6 -2
  974. scitex/scholar/url_finder/translators/individual/jisc_historical_texts.py +3 -1
  975. scitex/scholar/url_finder/translators/individual/jstor.py +14 -12
  976. scitex/scholar/url_finder/translators/individual/korean_national_library.py +3 -1
  977. scitex/scholar/url_finder/translators/individual/la_times.py +3 -1
  978. scitex/scholar/url_finder/translators/individual/landesbibliographie_baden_wurttemberg.py +3 -1
  979. scitex/scholar/url_finder/translators/individual/legislative_insight.py +3 -1
  980. scitex/scholar/url_finder/translators/individual/libraries_tasmania.py +3 -1
  981. scitex/scholar/url_finder/translators/individual/library_catalog__koha_.py +3 -1
  982. scitex/scholar/url_finder/translators/individual/lingbuzz.py +2 -2
  983. scitex/scholar/url_finder/translators/individual/max_planck_institute_for_the_history_of_science_virtual_laboratory_library.py +3 -1
  984. scitex/scholar/url_finder/translators/individual/mdpi.py +12 -6
  985. scitex/scholar/url_finder/translators/individual/microbiology_society_journals.py +3 -1
  986. scitex/scholar/url_finder/translators/individual/midas_journals.py +3 -1
  987. scitex/scholar/url_finder/translators/individual/nagoya_university_opac.py +3 -1
  988. scitex/scholar/url_finder/translators/individual/nature_publishing_group.py +32 -19
  989. scitex/scholar/url_finder/translators/individual/ntsb_accident_reports.py +3 -1
  990. scitex/scholar/url_finder/translators/individual/openedition_journals.py +8 -4
  991. scitex/scholar/url_finder/translators/individual/orcid.py +16 -15
  992. scitex/scholar/url_finder/translators/individual/oxford.py +25 -19
  993. scitex/scholar/url_finder/translators/individual/oxford_dictionaries_premium.py +3 -1
  994. scitex/scholar/url_finder/translators/individual/ozon_ru.py +3 -1
  995. scitex/scholar/url_finder/translators/individual/plos.py +9 -12
  996. scitex/scholar/url_finder/translators/individual/polygon.py +3 -1
  997. scitex/scholar/url_finder/translators/individual/primo.py +3 -1
  998. scitex/scholar/url_finder/translators/individual/project_muse.py +3 -1
  999. scitex/scholar/url_finder/translators/individual/pubfactory_journals.py +3 -1
  1000. scitex/scholar/url_finder/translators/individual/pubmed.py +71 -65
  1001. scitex/scholar/url_finder/translators/individual/pubmed_central.py +8 -6
  1002. scitex/scholar/url_finder/translators/individual/rechtspraak_nl.py +3 -1
  1003. scitex/scholar/url_finder/translators/individual/sage_journals.py +25 -17
  1004. scitex/scholar/url_finder/translators/individual/sciencedirect.py +36 -17
  1005. scitex/scholar/url_finder/translators/individual/semantics_visual_library.py +3 -1
  1006. scitex/scholar/url_finder/translators/individual/silverchair.py +70 -52
  1007. scitex/scholar/url_finder/translators/individual/sora.py +3 -1
  1008. scitex/scholar/url_finder/translators/individual/springer.py +15 -11
  1009. scitex/scholar/url_finder/translators/individual/ssrn.py +3 -3
  1010. scitex/scholar/url_finder/translators/individual/stanford_encyclopedia_of_philosophy.py +3 -1
  1011. scitex/scholar/url_finder/translators/individual/superlib.py +3 -1
  1012. scitex/scholar/url_finder/translators/individual/treesearch.py +3 -1
  1013. scitex/scholar/url_finder/translators/individual/university_of_chicago_press_books.py +3 -1
  1014. scitex/scholar/url_finder/translators/individual/vlex.py +3 -1
  1015. scitex/scholar/url_finder/translators/individual/web_of_science.py +3 -1
  1016. scitex/scholar/url_finder/translators/individual/web_of_science_nextgen.py +3 -1
  1017. scitex/scholar/url_finder/translators/individual/wiley.py +31 -25
  1018. scitex/scholar/url_finder/translators/individual/wilson_center_digital_archive.py +3 -1
  1019. scitex/scholar/utils/bibtex/_parse_bibtex.py +3 -3
  1020. scitex/scholar/utils/cleanup/_cleanup_scholar_processes.py +5 -9
  1021. scitex/scholar/utils/text/_TextNormalizer.py +249 -176
  1022. scitex/scholar/utils/validation/DOIValidator.py +31 -28
  1023. scitex/scholar/utils/validation/__init__.py +0 -0
  1024. scitex/scholar/utils/validation/validate_library_dois.py +61 -57
  1025. scitex/scholar/zotero/__init__.py +1 -1
  1026. scitex/security/cli.py +7 -20
  1027. scitex/security/github.py +45 -32
  1028. scitex/session/__init__.py +8 -9
  1029. scitex/session/_decorator.py +49 -42
  1030. scitex/session/_lifecycle.py +39 -39
  1031. scitex/session/_manager.py +24 -20
  1032. scitex/sh/__init__.py +4 -3
  1033. scitex/sh/_execute.py +10 -7
  1034. scitex/sh/_security.py +3 -3
  1035. scitex/sh/_types.py +2 -3
  1036. scitex/stats/__init__.py +174 -6
  1037. scitex/stats/_schema.py +42 -569
  1038. scitex/stats/auto/__init__.py +188 -0
  1039. scitex/stats/auto/_context.py +331 -0
  1040. scitex/stats/auto/_formatting.py +679 -0
  1041. scitex/stats/auto/_rules.py +901 -0
  1042. scitex/stats/auto/_selector.py +554 -0
  1043. scitex/stats/auto/_styles.py +721 -0
  1044. scitex/stats/correct/__init__.py +4 -4
  1045. scitex/stats/correct/_correct_bonferroni.py +43 -34
  1046. scitex/stats/correct/_correct_fdr.py +14 -40
  1047. scitex/stats/correct/_correct_fdr_.py +39 -46
  1048. scitex/stats/correct/_correct_holm.py +14 -32
  1049. scitex/stats/correct/_correct_sidak.py +36 -21
  1050. scitex/stats/descriptive/_circular.py +20 -21
  1051. scitex/stats/descriptive/_describe.py +19 -5
  1052. scitex/stats/descriptive/_nan.py +5 -7
  1053. scitex/stats/descriptive/_real.py +4 -3
  1054. scitex/stats/effect_sizes/__init__.py +10 -11
  1055. scitex/stats/effect_sizes/_cliffs_delta.py +35 -32
  1056. scitex/stats/effect_sizes/_cohens_d.py +30 -31
  1057. scitex/stats/effect_sizes/_epsilon_squared.py +19 -22
  1058. scitex/stats/effect_sizes/_eta_squared.py +23 -27
  1059. scitex/stats/effect_sizes/_prob_superiority.py +18 -21
  1060. scitex/stats/io/__init__.py +29 -0
  1061. scitex/stats/io/_bundle.py +156 -0
  1062. scitex/stats/posthoc/__init__.py +3 -3
  1063. scitex/stats/posthoc/_dunnett.py +75 -55
  1064. scitex/stats/posthoc/_games_howell.py +61 -43
  1065. scitex/stats/posthoc/_tukey_hsd.py +42 -34
  1066. scitex/stats/power/__init__.py +2 -2
  1067. scitex/stats/power/_power.py +56 -56
  1068. scitex/stats/tests/__init__.py +1 -1
  1069. scitex/stats/tests/correlation/__init__.py +1 -1
  1070. scitex/stats/tests/correlation/_test_pearson.py +28 -38
  1071. scitex/stats/utils/__init__.py +14 -17
  1072. scitex/stats/utils/_effect_size.py +85 -78
  1073. scitex/stats/utils/_formatters.py +49 -43
  1074. scitex/stats/utils/_normalizers.py +7 -14
  1075. scitex/stats/utils/_power.py +56 -56
  1076. scitex/str/__init__.py +1 -0
  1077. scitex/str/_clean_path.py +3 -3
  1078. scitex/str/_factor_out_digits.py +86 -58
  1079. scitex/str/_format_plot_text.py +180 -111
  1080. scitex/str/_latex.py +19 -19
  1081. scitex/str/_latex_fallback.py +9 -10
  1082. scitex/str/_parse.py +3 -6
  1083. scitex/str/_print_debug.py +13 -13
  1084. scitex/str/_printc.py +2 -0
  1085. scitex/str/_search.py +3 -3
  1086. scitex/template/.legacy/_clone_project.py +9 -13
  1087. scitex/template/__init__.py +10 -2
  1088. scitex/template/_clone_project.py +7 -2
  1089. scitex/template/_copy.py +1 -0
  1090. scitex/template/_customize.py +3 -6
  1091. scitex/template/_git_strategy.py +2 -3
  1092. scitex/template/_rename.py +1 -0
  1093. scitex/template/clone_pip_project.py +6 -7
  1094. scitex/template/clone_research.py +7 -10
  1095. scitex/template/clone_singularity.py +6 -7
  1096. scitex/template/clone_writer_directory.py +6 -7
  1097. scitex/tex/__init__.py +4 -0
  1098. scitex/tex/_export.py +890 -0
  1099. scitex/tex/_preview.py +26 -11
  1100. scitex/tex/_to_vec.py +10 -7
  1101. scitex/torch/__init__.py +11 -1
  1102. scitex/types/_ArrayLike.py +2 -0
  1103. scitex/types/_is_listed_X.py +3 -3
  1104. scitex/units.py +110 -77
  1105. scitex/utils/_compress_hdf5.py +3 -3
  1106. scitex/utils/_email.py +8 -4
  1107. scitex/utils/_notify.py +14 -8
  1108. scitex/utils/_search.py +6 -6
  1109. scitex/utils/_verify_scitex_format.py +17 -42
  1110. scitex/utils/_verify_scitex_format_v01.py +12 -34
  1111. scitex/utils/template.py +4 -3
  1112. scitex/web/__init__.py +7 -1
  1113. scitex/web/_scraping.py +54 -38
  1114. scitex/web/_search_pubmed.py +30 -14
  1115. scitex/writer/.legacy/Writer_v01-refactored.py +4 -4
  1116. scitex/writer/.legacy/_compile.py +18 -28
  1117. scitex/writer/Writer.py +8 -21
  1118. scitex/writer/__init__.py +11 -11
  1119. scitex/writer/_clone_writer_project.py +2 -6
  1120. scitex/writer/_compile/__init__.py +1 -0
  1121. scitex/writer/_compile/_parser.py +1 -0
  1122. scitex/writer/_compile/_runner.py +35 -38
  1123. scitex/writer/_compile/_validator.py +1 -0
  1124. scitex/writer/_compile/manuscript.py +1 -0
  1125. scitex/writer/_compile/revision.py +1 -0
  1126. scitex/writer/_compile/supplementary.py +1 -0
  1127. scitex/writer/_compile_async.py +5 -12
  1128. scitex/writer/_project/__init__.py +1 -0
  1129. scitex/writer/_project/_create.py +10 -25
  1130. scitex/writer/_project/_trees.py +4 -9
  1131. scitex/writer/_project/_validate.py +2 -3
  1132. scitex/writer/_validate_tree_structures.py +7 -18
  1133. scitex/writer/dataclasses/__init__.py +8 -10
  1134. scitex/writer/dataclasses/config/_CONSTANTS.py +2 -3
  1135. scitex/writer/dataclasses/config/_WriterConfig.py +4 -9
  1136. scitex/writer/dataclasses/contents/_ManuscriptContents.py +14 -25
  1137. scitex/writer/dataclasses/contents/_RevisionContents.py +21 -16
  1138. scitex/writer/dataclasses/contents/_SupplementaryContents.py +21 -24
  1139. scitex/writer/dataclasses/core/_Document.py +2 -3
  1140. scitex/writer/dataclasses/core/_DocumentSection.py +8 -23
  1141. scitex/writer/dataclasses/results/_CompilationResult.py +2 -3
  1142. scitex/writer/dataclasses/results/_LaTeXIssue.py +3 -6
  1143. scitex/writer/dataclasses/results/_SaveSectionsResponse.py +20 -9
  1144. scitex/writer/dataclasses/results/_SectionReadResponse.py +24 -10
  1145. scitex/writer/dataclasses/tree/_ConfigTree.py +7 -4
  1146. scitex/writer/dataclasses/tree/_ManuscriptTree.py +10 -13
  1147. scitex/writer/dataclasses/tree/_RevisionTree.py +16 -17
  1148. scitex/writer/dataclasses/tree/_ScriptsTree.py +10 -5
  1149. scitex/writer/dataclasses/tree/_SharedTree.py +10 -13
  1150. scitex/writer/dataclasses/tree/_SupplementaryTree.py +15 -14
  1151. scitex/writer/utils/.legacy_git_retry.py +3 -8
  1152. scitex/writer/utils/_parse_latex_logs.py +2 -3
  1153. scitex/writer/utils/_parse_script_args.py +20 -23
  1154. scitex/writer/utils/_watch.py +5 -5
  1155. {scitex-2.5.0.dist-info → scitex-2.7.3.dist-info}/METADATA +14 -10
  1156. {scitex-2.5.0.dist-info → scitex-2.7.3.dist-info}/RECORD +1149 -985
  1157. scitex/db/_sqlite3/_SQLite3Mixins/_ColumnMixin_v01-indentation-issues.py +0 -583
  1158. scitex/io/memo.md +0 -2827
  1159. scitex/plt/_subplots/TODO.md +0 -53
  1160. scitex/plt/_subplots/_AxisWrapperMixins/_AdjustmentMixin.py +0 -537
  1161. scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin.py +0 -1499
  1162. scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin.py +0 -431
  1163. scitex/plt/_subplots/_export_as_csv_formatters.py +0 -112
  1164. scitex/vis/__init__.py +0 -177
  1165. scitex/vis/editor/_defaults.py +0 -244
  1166. scitex/vis/editor/_edit.py +0 -378
  1167. scitex/vis/editor/flask_editor/__init__.py +0 -21
  1168. scitex/vis/editor/flask_editor/bbox.py +0 -216
  1169. scitex/vis/editor/flask_editor/core.py +0 -152
  1170. scitex/vis/editor/flask_editor/plotter.py +0 -130
  1171. scitex/vis/editor/flask_editor/renderer.py +0 -184
  1172. scitex/vis/editor/flask_editor/templates/html.py +0 -295
  1173. scitex/vis/editor/flask_editor/templates/scripts.py +0 -614
  1174. scitex/vis/editor/flask_editor/templates/styles.py +0 -549
  1175. /scitex/{vis → fig}/README.md +0 -0
  1176. /scitex/{vis → fig}/docs/CANVAS_ARCHITECTURE.md +0 -0
  1177. {scitex-2.5.0.dist-info → scitex-2.7.3.dist-info}/WHEEL +0 -0
  1178. {scitex-2.5.0.dist-info → scitex-2.7.3.dist-info}/entry_points.txt +0 -0
  1179. {scitex-2.5.0.dist-info → scitex-2.7.3.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,3136 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # File: ./src/scitex/vis/editor/flask_editor/templates/scripts.py
4
+ """JavaScript for the Flask editor UI."""
5
+
6
+ JS_SCRIPTS = """
7
+ let overrides = {{ overrides|safe }};
8
+ let traces = overrides.traces || [];
9
+ let elementBboxes = {};
10
+ let imgSize = {width: 0, height: 0};
11
+ let hoveredElement = null;
12
+ let selectedElement = null;
13
+ let debugMode = false; // Debug mode to show all hit areas
14
+ let isShowingOriginalPreview = false; // True when showing existing SVG/PNG from bundle
15
+ let originalBboxes = null; // Store original bboxes from /preview
16
+ let originalImgSize = null; // Store original img size from /preview
17
+
18
+ // Schema v0.3 metadata for axes-local coordinate transforms
19
+ let schemaMeta = null;
20
+
21
+ // Multi-panel state
22
+ let panelData = null; // Panel info from /preview
23
+ let currentPanelIndex = 0;
24
+ let showingPanelGrid = false;
25
+ let panelBboxesCache = {}; // Cache bboxes per panel {panelName: {bboxes, imgSize}}
26
+ let activePanelCard = null; // Currently active panel card for hover/click
27
+ let panelHoveredElement = null; // Hovered element in panel grid
28
+ let panelDebugMode = false; // Show hit regions in panel grid
29
+
30
+ // Cycle selection state for overlapping elements
31
+ let elementsAtCursor = []; // All elements at current cursor position
32
+ let currentCycleIndex = 0; // Current index in cycle
33
+
34
+ // Unit system state (default: mm)
35
+ let dimensionUnit = 'mm';
36
+ const MM_TO_INCH = 1 / 25.4;
37
+ const INCH_TO_MM = 25.4;
38
+
39
+ // Dark mode detection
40
+ function isDarkMode() {
41
+ return document.documentElement.getAttribute('data-theme') === 'dark';
42
+ }
43
+
44
+ // Hitmap-based element detection
45
+ let hitmapCanvas = null;
46
+ let hitmapCtx = null;
47
+ let hitmapColorMap = {}; // Maps RGB string -> element info
48
+ let hitmapLoaded = false;
49
+ let hitmapImgSize = {width: 0, height: 0};
50
+
51
+ // Load hitmap for pixel-based element detection
52
+ async function loadHitmap() {
53
+ try {
54
+ const resp = await fetch('/hitmap');
55
+ const data = await resp.json();
56
+
57
+ if (data.error) {
58
+ console.log('Hitmap not available:', data.error);
59
+ return;
60
+ }
61
+
62
+ // Create hidden canvas for hitmap sampling
63
+ hitmapCanvas = document.createElement('canvas');
64
+ hitmapCtx = hitmapCanvas.getContext('2d', { willReadFrequently: true });
65
+
66
+ // Load hitmap image
67
+ const img = new Image();
68
+ img.onload = () => {
69
+ hitmapCanvas.width = img.width;
70
+ hitmapCanvas.height = img.height;
71
+ hitmapImgSize = {width: img.width, height: img.height};
72
+ hitmapCtx.drawImage(img, 0, 0);
73
+ hitmapLoaded = true;
74
+ console.log('Hitmap loaded:', img.width, 'x', img.height);
75
+ };
76
+ img.src = 'data:image/png;base64,' + data.image;
77
+
78
+ // Build color map from response
79
+ if (data.color_map) {
80
+ for (const [key, info] of Object.entries(data.color_map)) {
81
+ if (info.rgb) {
82
+ const rgbKey = `${info.rgb[0]},${info.rgb[1]},${info.rgb[2]}`;
83
+ hitmapColorMap[rgbKey] = {
84
+ id: info.id,
85
+ type: info.type,
86
+ label: info.label,
87
+ axes_index: info.axes_index,
88
+ };
89
+ }
90
+ }
91
+ console.log('Hitmap color map:', Object.keys(hitmapColorMap).length, 'elements');
92
+ }
93
+ } catch (e) {
94
+ console.error('Error loading hitmap:', e);
95
+ }
96
+ }
97
+
98
+ // Get element at position using hitmap
99
+ function getElementFromHitmap(imgX, imgY) {
100
+ if (!hitmapLoaded || !hitmapCtx) return null;
101
+
102
+ // Scale coordinates from display image to hitmap
103
+ const scaleX = hitmapImgSize.width / imgSize.width;
104
+ const scaleY = hitmapImgSize.height / imgSize.height;
105
+ const hitmapX = Math.floor(imgX * scaleX);
106
+ const hitmapY = Math.floor(imgY * scaleY);
107
+
108
+ // Bounds check
109
+ if (hitmapX < 0 || hitmapX >= hitmapImgSize.width ||
110
+ hitmapY < 0 || hitmapY >= hitmapImgSize.height) {
111
+ return null;
112
+ }
113
+
114
+ // Sample pixel from hitmap
115
+ const pixel = hitmapCtx.getImageData(hitmapX, hitmapY, 1, 1).data;
116
+ const r = pixel[0], g = pixel[1], b = pixel[2], a = pixel[3];
117
+
118
+ // Skip transparent or white pixels
119
+ if (a < 128 || (r > 250 && g > 250 && b > 250)) {
120
+ return null;
121
+ }
122
+
123
+ // Look up in color map
124
+ const rgbKey = `${r},${g},${b}`;
125
+ const element = hitmapColorMap[rgbKey];
126
+
127
+ if (element) {
128
+ return `trace_${element.id}`; // Return element key for selection
129
+ }
130
+
131
+ return null;
132
+ }
133
+
134
+ // Hover system - client-side hit testing
135
+ function initHoverSystem() {
136
+ const container = document.getElementById('preview-container');
137
+ const img = document.getElementById('preview-img');
138
+
139
+ img.addEventListener('mousemove', (e) => {
140
+ if (imgSize.width === 0 || imgSize.height === 0) return;
141
+
142
+ const rect = img.getBoundingClientRect();
143
+ const x = e.clientX - rect.left;
144
+ const y = e.clientY - rect.top;
145
+
146
+ const scaleX = imgSize.width / rect.width;
147
+ const scaleY = imgSize.height / rect.height;
148
+ const imgX = x * scaleX;
149
+ const imgY = y * scaleY;
150
+
151
+ const element = findElementAt(imgX, imgY);
152
+ if (element !== hoveredElement) {
153
+ hoveredElement = element;
154
+ updateOverlay();
155
+ }
156
+ });
157
+
158
+ img.addEventListener('mouseleave', () => {
159
+ hoveredElement = null;
160
+ updateOverlay();
161
+ });
162
+
163
+ img.addEventListener('click', (e) => {
164
+ const rect = img.getBoundingClientRect();
165
+ const x = e.clientX - rect.left;
166
+ const y = e.clientY - rect.top;
167
+ const scaleX = imgSize.width / rect.width;
168
+ const scaleY = imgSize.height / rect.height;
169
+ const imgX = x * scaleX;
170
+ const imgY = y * scaleY;
171
+
172
+ // Alt+click or find all overlapping elements
173
+ if (e.altKey) {
174
+ // Cycle through overlapping elements
175
+ const allElements = findAllElementsAt(imgX, imgY);
176
+ if (allElements.length > 0) {
177
+ // If cursor moved to different location, reset cycle
178
+ if (JSON.stringify(allElements) !== JSON.stringify(elementsAtCursor)) {
179
+ elementsAtCursor = allElements;
180
+ currentCycleIndex = 0;
181
+ } else {
182
+ // Cycle to next element
183
+ currentCycleIndex = (currentCycleIndex + 1) % elementsAtCursor.length;
184
+ }
185
+ selectedElement = elementsAtCursor[currentCycleIndex];
186
+ updateOverlay();
187
+ scrollToSection(selectedElement);
188
+
189
+ // Show cycle indicator in status
190
+ const total = elementsAtCursor.length;
191
+ const current = currentCycleIndex + 1;
192
+ console.log(`Cycle selection: ${current}/${total} - ${selectedElement}`);
193
+ }
194
+ } else if (hoveredElement) {
195
+ // Normal click - select hovered element
196
+ selectedElement = hoveredElement;
197
+ elementsAtCursor = []; // Reset cycle
198
+ currentCycleIndex = 0;
199
+ updateOverlay();
200
+ scrollToSection(selectedElement);
201
+ }
202
+ });
203
+
204
+ // Right-click for cycle selection menu
205
+ img.addEventListener('contextmenu', (e) => {
206
+ e.preventDefault();
207
+
208
+ const rect = img.getBoundingClientRect();
209
+ const x = e.clientX - rect.left;
210
+ const y = e.clientY - rect.top;
211
+ const scaleX = imgSize.width / rect.width;
212
+ const scaleY = imgSize.height / rect.height;
213
+ const imgX = x * scaleX;
214
+ const imgY = y * scaleY;
215
+
216
+ const allElements = findAllElementsAt(imgX, imgY);
217
+ if (allElements.length > 1) {
218
+ // Cycle to next element
219
+ if (JSON.stringify(allElements) !== JSON.stringify(elementsAtCursor)) {
220
+ elementsAtCursor = allElements;
221
+ currentCycleIndex = 0;
222
+ } else {
223
+ currentCycleIndex = (currentCycleIndex + 1) % elementsAtCursor.length;
224
+ }
225
+ selectedElement = elementsAtCursor[currentCycleIndex];
226
+ updateOverlay();
227
+ scrollToSection(selectedElement);
228
+
229
+ const total = elementsAtCursor.length;
230
+ const current = currentCycleIndex + 1;
231
+ console.log(`Right-click cycle: ${current}/${total} - ${selectedElement}`);
232
+ } else if (allElements.length === 1) {
233
+ selectedElement = allElements[0];
234
+ updateOverlay();
235
+ scrollToSection(selectedElement);
236
+ }
237
+ });
238
+
239
+ img.addEventListener('load', () => {
240
+ updateOverlay();
241
+ });
242
+ }
243
+
244
+ // Convert axes-local pixel coordinates to image coordinates
245
+ function axesLocalToImage(axLocalX, axLocalY, axesBbox) {
246
+ // axesBbox has: x, y, width, height in figure pixel coordinates
247
+ // The local editor uses tight layout which shifts coordinates
248
+ // For now we use the existing image coordinates from bboxes
249
+ return [axLocalX + axesBbox.x, axLocalY + axesBbox.y];
250
+ }
251
+
252
+ // Get geometry_px points converted to image coordinates
253
+ function getGeometryPoints(bbox) {
254
+ const geom = bbox.geometry_px;
255
+ if (!geom) return null;
256
+
257
+ // For scatter: use points array directly
258
+ if (geom.points && geom.points.length > 0) {
259
+ return {
260
+ type: 'scatter',
261
+ points: geom.points,
262
+ hitRadius: geom.hit_radius_px || 5
263
+ };
264
+ }
265
+
266
+ // For lines: use path_simplified
267
+ if (geom.path_simplified && geom.path_simplified.length > 0) {
268
+ return {
269
+ type: 'line',
270
+ points: geom.path_simplified,
271
+ linewidth: geom.linewidth_px || 1
272
+ };
273
+ }
274
+
275
+ // For fills/polygons: use polygon
276
+ if (geom.polygon && geom.polygon.length > 0) {
277
+ return {
278
+ type: 'polygon',
279
+ points: geom.polygon
280
+ };
281
+ }
282
+
283
+ return null;
284
+ }
285
+
286
+ function findElementAt(x, y) {
287
+ // Multi-panel aware hit detection with specificity hierarchy:
288
+ // 1. Data elements with legacy points - proximity detection (correct saved-image coords)
289
+ // 2. Small elements (labels, ticks, legends, bars, fills)
290
+ // 3. Panel bboxes - lowest priority (fallback)
291
+ // Note: geometry_px (v0.3) uses axes-local coords which need coordinate transformation
292
+
293
+ const PROXIMITY_THRESHOLD = 15;
294
+ const SCATTER_THRESHOLD = 20; // Larger threshold for scatter points
295
+
296
+ // First: Check for data elements using legacy points (in saved-image coordinates)
297
+ let closestDataElement = null;
298
+ let minDistance = Infinity;
299
+
300
+ for (const [name, bbox] of Object.entries(elementBboxes)) {
301
+ if (name === '_meta') continue; // Skip metadata entry
302
+
303
+ // Prioritize legacy points array (already in correct saved-image coordinates)
304
+ if (bbox.points && bbox.points.length > 0) {
305
+ // Check if cursor is within general bbox area first
306
+ if (x >= bbox.x0 - SCATTER_THRESHOLD && x <= bbox.x1 + SCATTER_THRESHOLD &&
307
+ y >= bbox.y0 - SCATTER_THRESHOLD && y <= bbox.y1 + SCATTER_THRESHOLD) {
308
+
309
+ const elementType = bbox.element_type || 'line';
310
+ let dist;
311
+
312
+ if (elementType === 'scatter') {
313
+ // For scatter, find distance to nearest point
314
+ dist = distanceToNearestPoint(x, y, bbox.points);
315
+ } else {
316
+ // For lines, find distance to line segments
317
+ dist = distanceToLine(x, y, bbox.points);
318
+ }
319
+
320
+ if (dist < minDistance) {
321
+ minDistance = dist;
322
+ closestDataElement = name;
323
+ }
324
+ }
325
+ }
326
+ }
327
+
328
+ // Use appropriate threshold based on element type
329
+ if (closestDataElement) {
330
+ const bbox = elementBboxes[closestDataElement];
331
+ const threshold = (bbox.element_type === 'scatter') ? SCATTER_THRESHOLD : PROXIMITY_THRESHOLD;
332
+ if (minDistance <= threshold) {
333
+ return closestDataElement;
334
+ }
335
+ }
336
+
337
+ // Second: Collect all bbox matches, excluding panels and data elements with points
338
+ const elementMatches = [];
339
+ const panelMatches = [];
340
+
341
+ for (const [name, bbox] of Object.entries(elementBboxes)) {
342
+ if (x >= bbox.x0 && x <= bbox.x1 && y >= bbox.y0 && y <= bbox.y1) {
343
+ const area = (bbox.x1 - bbox.x0) * (bbox.y1 - bbox.y0);
344
+ const isPanel = bbox.is_panel || name.endsWith('_panel');
345
+ const hasPoints = bbox.points && bbox.points.length > 0;
346
+
347
+ if (hasPoints) {
348
+ // Already handled above with proximity
349
+ continue;
350
+ } else if (isPanel) {
351
+ panelMatches.push({name, area, bbox});
352
+ } else {
353
+ elementMatches.push({name, area, bbox});
354
+ }
355
+ }
356
+ }
357
+
358
+ // Return smallest non-panel element if any
359
+ if (elementMatches.length > 0) {
360
+ elementMatches.sort((a, b) => a.area - b.area);
361
+ return elementMatches[0].name;
362
+ }
363
+
364
+ // Fallback to panel selection (useful for multi-panel figures)
365
+ if (panelMatches.length > 0) {
366
+ panelMatches.sort((a, b) => a.area - b.area);
367
+ return panelMatches[0].name;
368
+ }
369
+
370
+ return null;
371
+ }
372
+
373
+ function distanceToNearestPoint(px, py, points) {
374
+ // Find distance to nearest point in scatter
375
+ if (!Array.isArray(points) || points.length === 0) return Infinity;
376
+ let minDist = Infinity;
377
+ for (const pt of points) {
378
+ if (!Array.isArray(pt) || pt.length < 2) continue;
379
+ const [x, y] = pt;
380
+ const dist = Math.sqrt((px - x) ** 2 + (py - y) ** 2);
381
+ if (dist < minDist) minDist = dist;
382
+ }
383
+ return minDist;
384
+ }
385
+
386
+ function distanceToLine(px, py, points) {
387
+ if (!Array.isArray(points) || points.length < 2) return Infinity;
388
+ let minDist = Infinity;
389
+ for (let i = 0; i < points.length - 1; i++) {
390
+ const pt1 = points[i];
391
+ const pt2 = points[i + 1];
392
+ if (!Array.isArray(pt1) || pt1.length < 2) continue;
393
+ if (!Array.isArray(pt2) || pt2.length < 2) continue;
394
+ const [x1, y1] = pt1;
395
+ const [x2, y2] = pt2;
396
+ const dist = distanceToSegment(px, py, x1, y1, x2, y2);
397
+ if (dist < minDist) minDist = dist;
398
+ }
399
+ return minDist;
400
+ }
401
+
402
+ function distanceToSegment(px, py, x1, y1, x2, y2) {
403
+ const dx = x2 - x1;
404
+ const dy = y2 - y1;
405
+ const lenSq = dx * dx + dy * dy;
406
+
407
+ if (lenSq === 0) {
408
+ return Math.sqrt((px - x1) ** 2 + (py - y1) ** 2);
409
+ }
410
+
411
+ let t = ((px - x1) * dx + (py - y1) * dy) / lenSq;
412
+ t = Math.max(0, Math.min(1, t));
413
+
414
+ const projX = x1 + t * dx;
415
+ const projY = y1 + t * dy;
416
+
417
+ return Math.sqrt((px - projX) ** 2 + (py - projY) ** 2);
418
+ }
419
+
420
+ // Point-in-polygon test using ray casting algorithm
421
+ function pointInPolygon(px, py, polygon) {
422
+ if (!Array.isArray(polygon) || polygon.length < 3) return false;
423
+
424
+ let inside = false;
425
+ for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
426
+ const ptI = polygon[i];
427
+ const ptJ = polygon[j];
428
+ if (!Array.isArray(ptI) || ptI.length < 2) continue;
429
+ if (!Array.isArray(ptJ) || ptJ.length < 2) continue;
430
+ const [xi, yi] = ptI;
431
+ const [xj, yj] = ptJ;
432
+
433
+ if (((yi > py) !== (yj > py)) &&
434
+ (px < (xj - xi) * (py - yi) / (yj - yi) + xi)) {
435
+ inside = !inside;
436
+ }
437
+ }
438
+ return inside;
439
+ }
440
+
441
+ function findAllElementsAt(x, y) {
442
+ // Find all elements at cursor position (for cycle selection)
443
+ // Returns array sorted by specificity (most specific first)
444
+ const PROXIMITY_THRESHOLD = 15;
445
+ const SCATTER_THRESHOLD = 20;
446
+
447
+ const results = [];
448
+
449
+ for (const [name, bbox] of Object.entries(elementBboxes)) {
450
+ let match = false;
451
+ let distance = Infinity;
452
+ let priority = 0; // Lower = more specific
453
+
454
+ const hasPoints = bbox.points && bbox.points.length > 0;
455
+ const elementType = bbox.element_type || '';
456
+ const isPanel = bbox.is_panel || name.endsWith('_panel');
457
+
458
+ // Check data elements with points (lines, scatter)
459
+ if (hasPoints) {
460
+ if (x >= bbox.x0 - SCATTER_THRESHOLD && x <= bbox.x1 + SCATTER_THRESHOLD &&
461
+ y >= bbox.y0 - SCATTER_THRESHOLD && y <= bbox.y1 + SCATTER_THRESHOLD) {
462
+
463
+ if (elementType === 'scatter') {
464
+ distance = distanceToNearestPoint(x, y, bbox.points);
465
+ if (distance <= SCATTER_THRESHOLD) {
466
+ match = true;
467
+ priority = 1; // Scatter points = high priority
468
+ }
469
+ } else {
470
+ distance = distanceToLine(x, y, bbox.points);
471
+ if (distance <= PROXIMITY_THRESHOLD) {
472
+ match = true;
473
+ priority = 2; // Lines = high priority
474
+ }
475
+ }
476
+ }
477
+ }
478
+
479
+ // Check bbox containment
480
+ if (x >= bbox.x0 && x <= bbox.x1 && y >= bbox.y0 && y <= bbox.y1) {
481
+ const area = (bbox.x1 - bbox.x0) * (bbox.y1 - bbox.y0);
482
+
483
+ if (!match) {
484
+ match = true;
485
+ distance = 0;
486
+ }
487
+
488
+ if (isPanel) {
489
+ priority = 100; // Panels = lowest priority
490
+ } else if (!hasPoints) {
491
+ // Small elements like labels, ticks - use area for priority
492
+ priority = 10 + Math.min(area / 10000, 50);
493
+ }
494
+ }
495
+
496
+ if (match) {
497
+ results.push({ name, distance, priority, bbox });
498
+ }
499
+ }
500
+
501
+ // Sort by priority (lower first), then by distance
502
+ results.sort((a, b) => {
503
+ if (a.priority !== b.priority) return a.priority - b.priority;
504
+ return a.distance - b.distance;
505
+ });
506
+
507
+ return results.map(r => r.name);
508
+ }
509
+
510
+ function drawTracePath(bbox, scaleX, scaleY, type) {
511
+ if (!Array.isArray(bbox.points) || bbox.points.length < 2) return '';
512
+
513
+ const points = bbox.points.filter(pt => Array.isArray(pt) && pt.length >= 2);
514
+ if (points.length < 2) return '';
515
+
516
+ let pathD = `M ${points[0][0] * scaleX} ${points[0][1] * scaleY}`;
517
+ for (let i = 1; i < points.length; i++) {
518
+ pathD += ` L ${points[i][0] * scaleX} ${points[i][1] * scaleY}`;
519
+ }
520
+
521
+ const className = type === 'hover' ? 'hover-path' : 'selected-path';
522
+ const labelX = points[0][0] * scaleX;
523
+ const labelY = points[0][1] * scaleY - 8;
524
+ const labelClass = type === 'hover' ? 'hover-label' : 'selected-label';
525
+
526
+ return `<path class="${className}" d="${pathD}"/>` +
527
+ `<text class="${labelClass}" x="${labelX}" y="${labelY}">${bbox.label || ''}</text>`;
528
+ }
529
+
530
+ function drawScatterPoints(bbox, scaleX, scaleY, type) {
531
+ // Draw scatter points as circles
532
+ if (!Array.isArray(bbox.points) || bbox.points.length === 0) return '';
533
+
534
+ const className = type === 'hover' ? 'hover-scatter' : 'selected-scatter';
535
+ const labelClass = type === 'hover' ? 'hover-label' : 'selected-label';
536
+ const radius = 4;
537
+
538
+ let svg = '';
539
+ for (const pt of bbox.points) {
540
+ if (!Array.isArray(pt) || pt.length < 2) continue;
541
+ const [x, y] = pt;
542
+ svg += `<circle class="${className}" cx="${x * scaleX}" cy="${y * scaleY}" r="${radius}"/>`;
543
+ }
544
+
545
+ // Add label near first point
546
+ const validPoints = bbox.points.filter(pt => Array.isArray(pt) && pt.length >= 2);
547
+ if (validPoints.length > 0) {
548
+ const labelX = validPoints[0][0] * scaleX;
549
+ const labelY = validPoints[0][1] * scaleY - 10;
550
+ svg += `<text class="${labelClass}" x="${labelX}" y="${labelY}">${bbox.label || ''}</text>`;
551
+ }
552
+
553
+ return svg;
554
+ }
555
+
556
+ function updateOverlay() {
557
+ const overlay = document.getElementById('hover-overlay');
558
+ // Find the visible preview element (SVG wrapper or img)
559
+ const svgWrapper = document.getElementById('preview-svg-wrapper');
560
+ const imgEl = document.getElementById('preview-img');
561
+
562
+ let targetEl = null;
563
+ if (svgWrapper) {
564
+ targetEl = svgWrapper.querySelector('svg') || svgWrapper;
565
+ } else if (imgEl && imgEl.offsetWidth > 0) {
566
+ targetEl = imgEl;
567
+ }
568
+
569
+ if (!targetEl) {
570
+ console.log('updateOverlay: No visible target element');
571
+ return;
572
+ }
573
+
574
+ const rect = targetEl.getBoundingClientRect();
575
+ if (rect.width === 0 || rect.height === 0) {
576
+ console.log('updateOverlay: Target element has zero dimensions');
577
+ return;
578
+ }
579
+
580
+ // Guard against zero imgSize (can cause Infinity scale)
581
+ if (!imgSize || !imgSize.width || !imgSize.height || imgSize.width === 0 || imgSize.height === 0) {
582
+ console.log('updateOverlay: imgSize not set or zero', imgSize);
583
+ return;
584
+ }
585
+
586
+ overlay.setAttribute('width', rect.width);
587
+ overlay.setAttribute('height', rect.height);
588
+ overlay.style.width = rect.width + 'px';
589
+ overlay.style.height = rect.height + 'px';
590
+
591
+ // Position overlay over the target element
592
+ const containerRect = document.getElementById('preview-container').getBoundingClientRect();
593
+ overlay.style.left = (rect.left - containerRect.left) + 'px';
594
+ overlay.style.top = (rect.top - containerRect.top) + 'px';
595
+
596
+ const scaleX = rect.width / imgSize.width;
597
+ const scaleY = rect.height / imgSize.height;
598
+
599
+ console.log('updateOverlay: rect=', rect.width, 'x', rect.height, 'imgSize=', imgSize, 'scale=', scaleX, scaleY);
600
+
601
+ let svg = '';
602
+
603
+ // Debug mode: draw ALL bboxes
604
+ if (debugMode) {
605
+ svg += drawDebugBboxes(scaleX, scaleY);
606
+ }
607
+
608
+ function drawElement(elementName, type) {
609
+ const bbox = elementBboxes[elementName];
610
+ if (!bbox) return '';
611
+
612
+ const elementType = bbox.element_type || '';
613
+ const hasPoints = bbox.points && bbox.points.length > 0;
614
+
615
+ // Lines - draw as path
616
+ if ((elementType === 'line' || elementName.includes('trace_')) && hasPoints) {
617
+ return drawTracePath(bbox, scaleX, scaleY, type);
618
+ }
619
+ // Scatter - draw as circles
620
+ else if (elementType === 'scatter' && hasPoints) {
621
+ return drawScatterPoints(bbox, scaleX, scaleY, type);
622
+ }
623
+ // Default - draw bbox rectangle
624
+ else {
625
+ const rectClass = type === 'hover' ? 'hover-rect' : 'selected-rect';
626
+ const labelClass = type === 'hover' ? 'hover-label' : 'selected-label';
627
+ const x = bbox.x0 * scaleX - 2;
628
+ const y = bbox.y0 * scaleY - 2;
629
+ const w = (bbox.x1 - bbox.x0) * scaleX + 4;
630
+ const h = (bbox.y1 - bbox.y0) * scaleY + 4;
631
+ return `<rect class="${rectClass}" x="${x}" y="${y}" width="${w}" height="${h}" rx="2"/>` +
632
+ `<text class="${labelClass}" x="${x}" y="${y - 4}">${bbox.label || elementName}</text>`;
633
+ }
634
+ }
635
+
636
+ if (hoveredElement && hoveredElement !== selectedElement) {
637
+ svg += drawElement(hoveredElement, 'hover');
638
+ }
639
+
640
+ if (selectedElement) {
641
+ svg += drawElement(selectedElement, 'selected');
642
+ }
643
+
644
+ overlay.innerHTML = svg;
645
+ }
646
+
647
+ // Draw all bboxes for debugging
648
+ function drawDebugBboxes(scaleX, scaleY) {
649
+ let svg = '';
650
+ let count = 0;
651
+
652
+ console.log('=== DEBUG BBOXES ===');
653
+ console.log('imgSize:', imgSize);
654
+ console.log('scale:', scaleX, scaleY);
655
+
656
+ for (const [name, bbox] of Object.entries(elementBboxes)) {
657
+ if (name === '_meta') continue;
658
+
659
+ count++;
660
+ const hasPoints = bbox.points && bbox.points.length > 0;
661
+ const elementType = bbox.element_type || '';
662
+
663
+ // Choose color based on element type
664
+ let rectClass = 'debug-rect';
665
+ if (name.includes('trace_') || elementType === 'line') {
666
+ rectClass = 'debug-rect-trace';
667
+ } else if (name.includes('legend')) {
668
+ rectClass = 'debug-rect-legend';
669
+ }
670
+
671
+ // Draw bbox rectangle
672
+ const x = bbox.x0 * scaleX;
673
+ const y = bbox.y0 * scaleY;
674
+ const w = (bbox.x1 - bbox.x0) * scaleX;
675
+ const h = (bbox.y1 - bbox.y0) * scaleY;
676
+
677
+ svg += `<rect class="${rectClass}" x="${x}" y="${y}" width="${w}" height="${h}"/>`;
678
+ svg += `<text class="debug-label" x="${x + 2}" y="${y + 10}">${name}</text>`;
679
+
680
+ // Draw path points if available
681
+ if (hasPoints && bbox.points.length > 1) {
682
+ let pathD = `M ${bbox.points[0][0] * scaleX} ${bbox.points[0][1] * scaleY}`;
683
+ for (let i = 1; i < bbox.points.length; i++) {
684
+ const pt = bbox.points[i];
685
+ if (pt && pt.length >= 2) {
686
+ pathD += ` L ${pt[0] * scaleX} ${pt[1] * scaleY}`;
687
+ }
688
+ }
689
+ svg += `<path class="debug-path" d="${pathD}"/>`;
690
+ }
691
+
692
+ console.log(` ${name}: (${bbox.x0?.toFixed(1)}, ${bbox.y0?.toFixed(1)}) - (${bbox.x1?.toFixed(1)}, ${bbox.y1?.toFixed(1)}), points: ${bbox.points?.length || 0}`);
693
+ }
694
+
695
+ console.log(`Total elements: ${count}`);
696
+ return svg;
697
+ }
698
+
699
+ // Toggle debug mode
700
+ function toggleDebugMode() {
701
+ debugMode = !debugMode;
702
+ const btn = document.getElementById('debug-toggle-btn');
703
+ if (btn) {
704
+ btn.classList.toggle('active', debugMode);
705
+ btn.textContent = debugMode ? 'Hide Hit Areas' : 'Show Hit Areas';
706
+ }
707
+ updateOverlay();
708
+ console.log('Debug mode:', debugMode ? 'ON' : 'OFF');
709
+ }
710
+
711
+ function expandSection(sectionId) {
712
+ console.log('expandSection called with:', sectionId);
713
+ let foundSection = null;
714
+ document.querySelectorAll('.section').forEach(section => {
715
+ const header = section.querySelector('.section-header');
716
+ const content = section.querySelector('.section-content');
717
+ if (section.id === sectionId) {
718
+ foundSection = section;
719
+ console.log('expandSection: Found section', sectionId, 'header:', header, 'content:', content);
720
+ header?.classList.remove('collapsed');
721
+ content?.classList.remove('collapsed');
722
+ } else if (header?.classList.contains('section-toggle')) {
723
+ header?.classList.add('collapsed');
724
+ content?.classList.add('collapsed');
725
+ }
726
+ });
727
+ if (!foundSection) {
728
+ console.warn('expandSection: Section not found:', sectionId);
729
+ } else {
730
+ // Scroll the section into view
731
+ setTimeout(() => {
732
+ foundSection.scrollIntoView({behavior: 'smooth', block: 'start'});
733
+ }, 50);
734
+ }
735
+ }
736
+
737
+ function scrollToSection(elementName) {
738
+ console.log('scrollToSection called with:', elementName);
739
+
740
+ // Map element names to their corresponding sections
741
+ // Elements with a mapping here will NOT show the "Selected" panel
742
+ const elementToSection = {
743
+ 'title': 'section-labels',
744
+ 'xlabel': 'section-labels',
745
+ 'ylabel': 'section-labels',
746
+ 'caption': 'section-labels',
747
+ 'xaxis': 'section-ticks',
748
+ 'yaxis': 'section-ticks',
749
+ 'xaxis_ticks': 'section-ticks',
750
+ 'yaxis_ticks': 'section-ticks',
751
+ 'xaxis_spine': 'section-ticks',
752
+ 'yaxis_spine': 'section-ticks',
753
+ 'legend': 'section-legend'
754
+ };
755
+
756
+ const fieldMap = {
757
+ 'title': 'title',
758
+ 'xlabel': 'xlabel',
759
+ 'ylabel': 'ylabel',
760
+ 'caption': 'caption',
761
+ 'xaxis': 'xmin',
762
+ 'yaxis': 'ymin',
763
+ 'xaxis_ticks': 'x_tick_fontsize',
764
+ 'yaxis_ticks': 'y_tick_fontsize',
765
+ 'xaxis_spine': 'axis_width',
766
+ 'yaxis_spine': 'axis_width',
767
+ 'legend': 'legend_visible'
768
+ };
769
+
770
+ if (elementName.startsWith('trace_')) {
771
+ expandSection('section-traces');
772
+ const traceIdx = elementBboxes[elementName]?.trace_idx;
773
+ if (traceIdx !== undefined) {
774
+ const traceItems = document.querySelectorAll('.trace-item');
775
+ if (traceItems[traceIdx]) {
776
+ setTimeout(() => {
777
+ // Scroll into view and highlight the trace item
778
+ traceItems[traceIdx].scrollIntoView({behavior: 'smooth', block: 'center'});
779
+ // Add temporary highlight effect
780
+ traceItems[traceIdx].classList.add('trace-item-highlight');
781
+ setTimeout(() => {
782
+ traceItems[traceIdx].classList.remove('trace-item-highlight');
783
+ }, 1500);
784
+ }, 100);
785
+ }
786
+ }
787
+ return;
788
+ }
789
+
790
+ // Extract base element name from prefixed names like "ax_00_yaxis_spine" or "ax0_title"
791
+ let baseElementName = elementName;
792
+ const match = elementName.match(/ax_?\d+_(.+)/);
793
+ if (match) {
794
+ baseElementName = match[1];
795
+ }
796
+ console.log('scrollToSection: baseElementName=', baseElementName, 'from', elementName);
797
+
798
+ const sectionId = elementToSection[baseElementName];
799
+ const fieldId = fieldMap[baseElementName];
800
+ console.log('scrollToSection: sectionId=', sectionId, 'fieldId=', fieldId);
801
+
802
+ if (sectionId) {
803
+ console.log('scrollToSection: expanding section', sectionId);
804
+ // Element has a corresponding section - expand it, don't show "Selected" panel
805
+ expandSection(sectionId);
806
+
807
+ if (fieldId) {
808
+ const field = document.getElementById(fieldId);
809
+ if (field) {
810
+ setTimeout(() => {
811
+ field.scrollIntoView({behavior: 'smooth', block: 'center'});
812
+ field.focus();
813
+ }, 100);
814
+ }
815
+ }
816
+
817
+ // Hide the "Selected" panel since we're using existing section
818
+ const selectedSection = document.getElementById('section-selected');
819
+ if (selectedSection) {
820
+ selectedSection.style.display = 'none';
821
+ }
822
+ } else {
823
+ // No corresponding section - show the "Selected" panel for this element
824
+ showSelectedElementPanel(elementName);
825
+ }
826
+ }
827
+
828
+ // Field to element synchronization - highlight element when field is focused
829
+ function setupFieldToElementSync() {
830
+ // Map field IDs to element names
831
+ const fieldToElement = {
832
+ // Title, Labels & Caption section
833
+ 'title': 'title',
834
+ 'title_fontsize': 'title',
835
+ 'show_title': 'title',
836
+ 'xlabel': 'xlabel',
837
+ 'ylabel': 'ylabel',
838
+ 'caption': 'caption',
839
+ 'caption_fontsize': 'caption',
840
+ 'show_caption': 'caption',
841
+ // Axis & Ticks section
842
+ 'xmin': 'xaxis',
843
+ 'xmax': 'xaxis',
844
+ 'ymin': 'yaxis',
845
+ 'ymax': 'yaxis',
846
+ 'x_n_ticks': 'xaxis_ticks',
847
+ 'hide_x_ticks': 'xaxis_ticks',
848
+ 'x_tick_fontsize': 'xaxis_ticks',
849
+ 'x_tick_direction': 'xaxis_ticks',
850
+ 'x_tick_length': 'xaxis_ticks',
851
+ 'x_tick_width': 'xaxis_ticks',
852
+ 'y_n_ticks': 'yaxis_ticks',
853
+ 'hide_y_ticks': 'yaxis_ticks',
854
+ 'y_tick_fontsize': 'yaxis_ticks',
855
+ 'y_tick_direction': 'yaxis_ticks',
856
+ 'y_tick_length': 'yaxis_ticks',
857
+ 'y_tick_width': 'yaxis_ticks',
858
+ // Legend section
859
+ 'legend_visible': 'legend',
860
+ 'legend_loc': 'legend',
861
+ 'legend_frameon': 'legend',
862
+ 'legend_fontsize': 'legend',
863
+ 'legend_ncols': 'legend',
864
+ 'legend_x': 'legend',
865
+ 'legend_y': 'legend'
866
+ };
867
+
868
+ // Add focus listeners to all mapped fields
869
+ Object.entries(fieldToElement).forEach(([fieldId, elementName]) => {
870
+ const field = document.getElementById(fieldId);
871
+ if (field) {
872
+ field.addEventListener('focus', () => {
873
+ // Find the element in bboxes - for multi-panel, check ax_00 first
874
+ let targetElement = null;
875
+ if (elementBboxes[elementName]) {
876
+ targetElement = elementName;
877
+ } else {
878
+ // Try to find with axis prefix (e.g., ax_00_title)
879
+ for (const key of Object.keys(elementBboxes)) {
880
+ if (key.endsWith('_' + elementName) || key === elementName) {
881
+ targetElement = key;
882
+ break;
883
+ }
884
+ }
885
+ }
886
+
887
+ if (targetElement) {
888
+ selectedElement = targetElement;
889
+ updateOverlay();
890
+ setStatus(`Highlighting: ${targetElement}`, false);
891
+ }
892
+ });
893
+
894
+ // Also handle mouseenter for hover feedback
895
+ field.addEventListener('mouseenter', () => {
896
+ let targetElement = null;
897
+ if (elementBboxes[elementName]) {
898
+ targetElement = elementName;
899
+ } else {
900
+ for (const key of Object.keys(elementBboxes)) {
901
+ if (key.endsWith('_' + elementName) || key === elementName) {
902
+ targetElement = key;
903
+ break;
904
+ }
905
+ }
906
+ }
907
+
908
+ if (targetElement && targetElement !== selectedElement) {
909
+ hoveredElement = targetElement;
910
+ updateOverlay();
911
+ }
912
+ });
913
+
914
+ field.addEventListener('mouseleave', () => {
915
+ if (hoveredElement && hoveredElement !== selectedElement) {
916
+ hoveredElement = null;
917
+ updateOverlay();
918
+ }
919
+ });
920
+ }
921
+ });
922
+ }
923
+
924
+ // Selected element panel management
925
+ function showSelectedElementPanel(elementName) {
926
+ const section = document.getElementById('section-selected');
927
+ const titleEl = document.getElementById('selected-element-title');
928
+ const typeBadge = document.getElementById('element-type-badge');
929
+ const axisInfo = document.getElementById('element-axis-info');
930
+
931
+ // Hide all property sections first
932
+ document.querySelectorAll('.element-props').forEach(el => el.style.display = 'none');
933
+
934
+ if (!elementName) {
935
+ section.style.display = 'none';
936
+ return;
937
+ }
938
+
939
+ section.style.display = 'block';
940
+
941
+ // Parse element name to extract type and info
942
+ const elementInfo = parseElementName(elementName);
943
+ const bbox = elementBboxes[elementName] || {};
944
+
945
+ // Update title
946
+ titleEl.textContent = `Selected: ${elementInfo.displayName}`;
947
+
948
+ // Update type badge
949
+ typeBadge.className = `element-type-badge ${elementInfo.type}`;
950
+ typeBadge.textContent = elementInfo.type;
951
+
952
+ // Update axis info
953
+ if (elementInfo.axisId) {
954
+ const row = elementInfo.axisId.match(/ax_(\\d)(\\d)/);
955
+ if (row) {
956
+ axisInfo.textContent = `Panel: Row ${parseInt(row[1])+1}, Col ${parseInt(row[2])+1}`;
957
+ } else {
958
+ axisInfo.textContent = `Axis: ${elementInfo.axisId}`;
959
+ }
960
+ } else {
961
+ axisInfo.textContent = '';
962
+ }
963
+
964
+ // Show appropriate property panel and populate with current values
965
+ showPropertiesForElement(elementInfo, bbox);
966
+ }
967
+
968
+ function parseElementName(name) {
969
+ // Parse names like: ax_00_scatter_0, ax_11_trace_1, ax_01_xlabel, trace_0, xlabel, ax_00_xaxis, etc.
970
+ const result = {
971
+ original: name,
972
+ type: 'unknown',
973
+ displayName: name,
974
+ axisId: null,
975
+ index: null
976
+ };
977
+
978
+ // Check for axis prefix (ax_XX_)
979
+ const axisMatch = name.match(/^(ax_\\d+)_(.+)$/);
980
+ if (axisMatch) {
981
+ result.axisId = axisMatch[1];
982
+ name = axisMatch[2]; // Rest of the name
983
+ }
984
+
985
+ // Determine element type
986
+ if (name.includes('scatter')) {
987
+ result.type = 'scatter';
988
+ const idx = name.match(/scatter_(\\d+)/);
989
+ result.index = idx ? parseInt(idx[1]) : 0;
990
+ result.displayName = `Scatter ${result.index + 1}`;
991
+ } else if (name.includes('trace')) {
992
+ result.type = 'trace';
993
+ const idx = name.match(/trace_(\\d+)/);
994
+ result.index = idx ? parseInt(idx[1]) : 0;
995
+ result.displayName = `Line ${result.index + 1}`;
996
+ } else if (name.includes('fill')) {
997
+ result.type = 'fill';
998
+ const idx = name.match(/fill_(\\d+)/);
999
+ result.index = idx ? parseInt(idx[1]) : 0;
1000
+ result.displayName = `Fill Area ${result.index + 1}`;
1001
+ } else if (name.includes('bar')) {
1002
+ result.type = 'bar';
1003
+ const idx = name.match(/bar_(\\d+)/);
1004
+ result.index = idx ? parseInt(idx[1]) : 0;
1005
+ result.displayName = `Bar ${result.index + 1}`;
1006
+ } else if (name === 'xlabel' || name === 'ylabel' || name === 'title') {
1007
+ result.type = 'label';
1008
+ result.displayName = name.charAt(0).toUpperCase() + name.slice(1);
1009
+ } else if (name === 'legend') {
1010
+ result.type = 'legend';
1011
+ result.displayName = 'Legend';
1012
+ } else if (name === 'xaxis') {
1013
+ result.type = 'xaxis';
1014
+ result.displayName = 'X-Axis';
1015
+ } else if (name === 'yaxis') {
1016
+ result.type = 'yaxis';
1017
+ result.displayName = 'Y-Axis';
1018
+ } else if (name.includes('panel')) {
1019
+ result.type = 'panel';
1020
+ result.displayName = 'Panel';
1021
+ }
1022
+
1023
+ return result;
1024
+ }
1025
+
1026
+ function showPropertiesForElement(elementInfo, bbox) {
1027
+ const type = elementInfo.type;
1028
+
1029
+ if (type === 'trace') {
1030
+ const props = document.getElementById('selected-trace-props');
1031
+ props.style.display = 'block';
1032
+
1033
+ // Get values from: 1) element_overrides, 2) traces array (pltz metadata), 3) bbox data
1034
+ const traceOverrides = getTraceOverrides(elementInfo);
1035
+ const traceIdx = elementInfo.index || 0;
1036
+ const traceFromMeta = traces[traceIdx] || {};
1037
+
1038
+ // Label: prefer user override, then pltz metadata, then bbox label
1039
+ const label = traceOverrides.label || traceFromMeta.label || bbox.label?.replace(/.*:\s*/, '') || '';
1040
+ const color = traceOverrides.color || traceFromMeta.color || '#1f77b4';
1041
+ const linewidth = traceOverrides.linewidth || traceFromMeta.linewidth || 1.0;
1042
+ const linestyle = traceOverrides.linestyle || traceFromMeta.linestyle || '-';
1043
+ const marker = traceOverrides.marker || traceFromMeta.marker || '';
1044
+ const markersize = traceOverrides.markersize || traceFromMeta.markersize || 4;
1045
+ const alpha = traceOverrides.alpha || traceFromMeta.alpha || 1;
1046
+
1047
+ document.getElementById('sel-trace-label').value = label;
1048
+ document.getElementById('sel-trace-color').value = color;
1049
+ document.getElementById('sel-trace-color-text').value = color;
1050
+ document.getElementById('sel-trace-linewidth').value = linewidth;
1051
+ document.getElementById('sel-trace-linestyle').value = linestyle;
1052
+ document.getElementById('sel-trace-marker').value = marker;
1053
+ document.getElementById('sel-trace-markersize').value = markersize;
1054
+ document.getElementById('sel-trace-alpha').value = alpha;
1055
+ } else if (type === 'scatter') {
1056
+ const props = document.getElementById('selected-scatter-props');
1057
+ props.style.display = 'block';
1058
+
1059
+ const scatterOverrides = getScatterOverrides(elementInfo);
1060
+ if (scatterOverrides) {
1061
+ document.getElementById('sel-scatter-color').value = scatterOverrides.color || '#1f77b4';
1062
+ document.getElementById('sel-scatter-color-text').value = scatterOverrides.color || '#1f77b4';
1063
+ document.getElementById('sel-scatter-size').value = scatterOverrides.size || 20;
1064
+ document.getElementById('sel-scatter-marker').value = scatterOverrides.marker || 'o';
1065
+ document.getElementById('sel-scatter-alpha').value = scatterOverrides.alpha || 0.7;
1066
+ document.getElementById('sel-scatter-edgecolor').value = scatterOverrides.edgecolor || '#000000';
1067
+ document.getElementById('sel-scatter-edgecolor-text').value = scatterOverrides.edgecolor || '#000000';
1068
+ }
1069
+ } else if (type === 'fill') {
1070
+ const props = document.getElementById('selected-fill-props');
1071
+ props.style.display = 'block';
1072
+
1073
+ const fillOverrides = getFillOverrides(elementInfo);
1074
+ if (fillOverrides) {
1075
+ document.getElementById('sel-fill-color').value = fillOverrides.color || '#1f77b4';
1076
+ document.getElementById('sel-fill-color-text').value = fillOverrides.color || '#1f77b4';
1077
+ document.getElementById('sel-fill-alpha').value = fillOverrides.alpha || 0.3;
1078
+ }
1079
+ } else if (type === 'bar') {
1080
+ const props = document.getElementById('selected-bar-props');
1081
+ props.style.display = 'block';
1082
+ } else if (type === 'label') {
1083
+ const props = document.getElementById('selected-label-props');
1084
+ props.style.display = 'block';
1085
+
1086
+ // Get label text from global overrides
1087
+ const labelName = elementInfo.displayName.toLowerCase();
1088
+ document.getElementById('sel-label-text').value = overrides[labelName] || '';
1089
+ document.getElementById('sel-label-fontsize').value = overrides.axis_fontsize || 7;
1090
+ } else if (type === 'panel') {
1091
+ const props = document.getElementById('selected-panel-props');
1092
+ props.style.display = 'block';
1093
+
1094
+ // Load existing panel overrides, fall back to actual bbox values
1095
+ const panelOverrides = getPanelOverrides(elementInfo);
1096
+ const panelBbox = elementBboxes[selectedElement] || {};
1097
+ document.getElementById('sel-panel-title').value = panelOverrides.title || panelBbox.title || '';
1098
+ document.getElementById('sel-panel-xlabel').value = panelOverrides.xlabel || panelBbox.xlabel || '';
1099
+ document.getElementById('sel-panel-ylabel').value = panelOverrides.ylabel || panelBbox.ylabel || '';
1100
+ } else if (type === 'legend') {
1101
+ // For legend, expand the legend section instead
1102
+ expandSection('section-legend');
1103
+ } else if (type === 'xaxis') {
1104
+ const props = document.getElementById('selected-xaxis-props');
1105
+ props.style.display = 'block';
1106
+
1107
+ // Load existing xaxis overrides
1108
+ const xaxisOverrides = getAxisOverrides(elementInfo, 'xaxis');
1109
+ document.getElementById('sel-xaxis-fontsize').value = xaxisOverrides.tick_fontsize || overrides.tick_fontsize || 7;
1110
+ document.getElementById('sel-xaxis-label-fontsize').value = xaxisOverrides.label_fontsize || overrides.axis_fontsize || 7;
1111
+ document.getElementById('sel-xaxis-direction').value = xaxisOverrides.tick_direction || overrides.tick_direction || 'out';
1112
+ document.getElementById('sel-xaxis-nticks').value = xaxisOverrides.n_ticks || overrides.x_n_ticks || 4;
1113
+ document.getElementById('sel-xaxis-hide-ticks').checked = xaxisOverrides.hide_ticks || false;
1114
+ document.getElementById('sel-xaxis-hide-label').checked = xaxisOverrides.hide_label || false;
1115
+ document.getElementById('sel-xaxis-hide-spine').checked = xaxisOverrides.hide_spine || false;
1116
+ } else if (type === 'yaxis') {
1117
+ const props = document.getElementById('selected-yaxis-props');
1118
+ props.style.display = 'block';
1119
+
1120
+ // Load existing yaxis overrides
1121
+ const yaxisOverrides = getAxisOverrides(elementInfo, 'yaxis');
1122
+ document.getElementById('sel-yaxis-fontsize').value = yaxisOverrides.tick_fontsize || overrides.tick_fontsize || 7;
1123
+ document.getElementById('sel-yaxis-label-fontsize').value = yaxisOverrides.label_fontsize || overrides.axis_fontsize || 7;
1124
+ document.getElementById('sel-yaxis-direction').value = yaxisOverrides.tick_direction || overrides.tick_direction || 'out';
1125
+ document.getElementById('sel-yaxis-nticks').value = yaxisOverrides.n_ticks || overrides.y_n_ticks || 4;
1126
+ document.getElementById('sel-yaxis-hide-ticks').checked = yaxisOverrides.hide_ticks || false;
1127
+ document.getElementById('sel-yaxis-hide-label').checked = yaxisOverrides.hide_label || false;
1128
+ document.getElementById('sel-yaxis-hide-spine').checked = yaxisOverrides.hide_spine || false;
1129
+ }
1130
+
1131
+ // Show statistics for data elements (trace, scatter, fill, bar)
1132
+ if (['trace', 'scatter', 'fill', 'bar'].includes(type)) {
1133
+ showElementStatistics(bbox);
1134
+ } else {
1135
+ hideElementStatistics();
1136
+ }
1137
+ }
1138
+
1139
+ function showElementStatistics(bbox) {
1140
+ const statsDiv = document.getElementById('selected-stats');
1141
+ if (!bbox || !bbox.points || bbox.points.length === 0) {
1142
+ statsDiv.style.display = 'none';
1143
+ return;
1144
+ }
1145
+
1146
+ statsDiv.style.display = 'block';
1147
+
1148
+ // Extract Y values from points (format: [[x,y], [x,y], ...])
1149
+ const yValues = bbox.points.map(pt => pt[1]).filter(v => isFinite(v));
1150
+ const xValues = bbox.points.map(pt => pt[0]).filter(v => isFinite(v));
1151
+
1152
+ if (yValues.length === 0) {
1153
+ statsDiv.style.display = 'none';
1154
+ return;
1155
+ }
1156
+
1157
+ // Calculate statistics
1158
+ const n = yValues.length;
1159
+ const mean = yValues.reduce((a, b) => a + b, 0) / n;
1160
+ const variance = yValues.reduce((sum, v) => sum + Math.pow(v - mean, 2), 0) / n;
1161
+ const std = Math.sqrt(variance);
1162
+ const min = Math.min(...yValues);
1163
+ const max = Math.max(...yValues);
1164
+ const range = max - min;
1165
+
1166
+ // Format numbers appropriately
1167
+ const fmt = (v) => {
1168
+ if (Math.abs(v) < 0.01 || Math.abs(v) >= 10000) {
1169
+ return v.toExponential(2);
1170
+ }
1171
+ return v.toFixed(2);
1172
+ };
1173
+
1174
+ // Update display
1175
+ document.getElementById('stat-n').textContent = n;
1176
+ document.getElementById('stat-mean').textContent = fmt(mean);
1177
+ document.getElementById('stat-std').textContent = fmt(std);
1178
+ document.getElementById('stat-min').textContent = fmt(min);
1179
+ document.getElementById('stat-max').textContent = fmt(max);
1180
+ document.getElementById('stat-range').textContent = fmt(range);
1181
+ }
1182
+
1183
+ function hideElementStatistics() {
1184
+ document.getElementById('selected-stats').style.display = 'none';
1185
+ }
1186
+
1187
+ function getTraceOverrides(elementInfo) {
1188
+ // Initialize element overrides storage if not exists
1189
+ if (!overrides.element_overrides) {
1190
+ overrides.element_overrides = {};
1191
+ }
1192
+
1193
+ const key = elementInfo.original;
1194
+ if (!overrides.element_overrides[key]) {
1195
+ // Try to get from traces array
1196
+ if (traces[elementInfo.index]) {
1197
+ overrides.element_overrides[key] = { ...traces[elementInfo.index] };
1198
+ } else {
1199
+ overrides.element_overrides[key] = {};
1200
+ }
1201
+ }
1202
+ return overrides.element_overrides[key];
1203
+ }
1204
+
1205
+ function getScatterOverrides(elementInfo) {
1206
+ if (!overrides.element_overrides) {
1207
+ overrides.element_overrides = {};
1208
+ }
1209
+ const key = elementInfo.original;
1210
+ if (!overrides.element_overrides[key]) {
1211
+ overrides.element_overrides[key] = {};
1212
+ }
1213
+ return overrides.element_overrides[key];
1214
+ }
1215
+
1216
+ function getFillOverrides(elementInfo) {
1217
+ if (!overrides.element_overrides) {
1218
+ overrides.element_overrides = {};
1219
+ }
1220
+ const key = elementInfo.original;
1221
+ if (!overrides.element_overrides[key]) {
1222
+ overrides.element_overrides[key] = {};
1223
+ }
1224
+ return overrides.element_overrides[key];
1225
+ }
1226
+
1227
+ function getPanelOverrides(elementInfo) {
1228
+ if (!overrides.element_overrides) {
1229
+ overrides.element_overrides = {};
1230
+ }
1231
+ const key = elementInfo.original;
1232
+ if (!overrides.element_overrides[key]) {
1233
+ overrides.element_overrides[key] = {};
1234
+ }
1235
+ return overrides.element_overrides[key];
1236
+ }
1237
+
1238
+ function getAxisOverrides(elementInfo, axisType) {
1239
+ // Get overrides for xaxis or yaxis element
1240
+ if (!overrides.element_overrides) {
1241
+ overrides.element_overrides = {};
1242
+ }
1243
+ const key = elementInfo.original;
1244
+ if (!overrides.element_overrides[key]) {
1245
+ overrides.element_overrides[key] = {};
1246
+ }
1247
+ return overrides.element_overrides[key];
1248
+ }
1249
+
1250
+ function applySelectedElementChanges() {
1251
+ if (!selectedElement) return;
1252
+
1253
+ const elementInfo = parseElementName(selectedElement);
1254
+ const type = elementInfo.type;
1255
+
1256
+ if (!overrides.element_overrides) {
1257
+ overrides.element_overrides = {};
1258
+ }
1259
+
1260
+ if (type === 'trace') {
1261
+ overrides.element_overrides[selectedElement] = {
1262
+ label: document.getElementById('sel-trace-label').value,
1263
+ color: document.getElementById('sel-trace-color').value,
1264
+ linewidth: parseFloat(document.getElementById('sel-trace-linewidth').value),
1265
+ linestyle: document.getElementById('sel-trace-linestyle').value,
1266
+ marker: document.getElementById('sel-trace-marker').value,
1267
+ markersize: parseFloat(document.getElementById('sel-trace-markersize').value),
1268
+ alpha: parseFloat(document.getElementById('sel-trace-alpha').value)
1269
+ };
1270
+ } else if (type === 'scatter') {
1271
+ overrides.element_overrides[selectedElement] = {
1272
+ color: document.getElementById('sel-scatter-color').value,
1273
+ size: parseFloat(document.getElementById('sel-scatter-size').value),
1274
+ marker: document.getElementById('sel-scatter-marker').value,
1275
+ alpha: parseFloat(document.getElementById('sel-scatter-alpha').value),
1276
+ edgecolor: document.getElementById('sel-scatter-edgecolor').value
1277
+ };
1278
+ } else if (type === 'fill') {
1279
+ overrides.element_overrides[selectedElement] = {
1280
+ color: document.getElementById('sel-fill-color').value,
1281
+ alpha: parseFloat(document.getElementById('sel-fill-alpha').value)
1282
+ };
1283
+ } else if (type === 'label') {
1284
+ const labelName = elementInfo.displayName.toLowerCase();
1285
+ overrides[labelName] = document.getElementById('sel-label-text').value;
1286
+ overrides.axis_fontsize = parseFloat(document.getElementById('sel-label-fontsize').value);
1287
+ } else if (type === 'bar') {
1288
+ overrides.element_overrides[selectedElement] = {
1289
+ facecolor: document.getElementById('sel-bar-facecolor').value,
1290
+ edgecolor: document.getElementById('sel-bar-edgecolor').value,
1291
+ alpha: parseFloat(document.getElementById('sel-bar-alpha').value)
1292
+ };
1293
+ } else if (type === 'panel') {
1294
+ // Panel-specific overrides (per-axis) including title, xlabel, ylabel
1295
+ overrides.element_overrides[selectedElement] = {
1296
+ title: document.getElementById('sel-panel-title').value,
1297
+ xlabel: document.getElementById('sel-panel-xlabel').value,
1298
+ ylabel: document.getElementById('sel-panel-ylabel').value,
1299
+ facecolor: document.getElementById('sel-panel-facecolor').value,
1300
+ transparent: document.getElementById('sel-panel-transparent').checked,
1301
+ grid: document.getElementById('sel-panel-grid').checked
1302
+ };
1303
+ } else if (type === 'xaxis') {
1304
+ // X-Axis specific overrides
1305
+ overrides.element_overrides[selectedElement] = {
1306
+ tick_fontsize: parseFloat(document.getElementById('sel-xaxis-fontsize').value),
1307
+ label_fontsize: parseFloat(document.getElementById('sel-xaxis-label-fontsize').value),
1308
+ tick_direction: document.getElementById('sel-xaxis-direction').value,
1309
+ n_ticks: parseInt(document.getElementById('sel-xaxis-nticks').value),
1310
+ hide_ticks: document.getElementById('sel-xaxis-hide-ticks').checked,
1311
+ hide_label: document.getElementById('sel-xaxis-hide-label').checked,
1312
+ hide_spine: document.getElementById('sel-xaxis-hide-spine').checked
1313
+ };
1314
+ } else if (type === 'yaxis') {
1315
+ // Y-Axis specific overrides
1316
+ overrides.element_overrides[selectedElement] = {
1317
+ tick_fontsize: parseFloat(document.getElementById('sel-yaxis-fontsize').value),
1318
+ label_fontsize: parseFloat(document.getElementById('sel-yaxis-label-fontsize').value),
1319
+ tick_direction: document.getElementById('sel-yaxis-direction').value,
1320
+ n_ticks: parseInt(document.getElementById('sel-yaxis-nticks').value),
1321
+ hide_ticks: document.getElementById('sel-yaxis-hide-ticks').checked,
1322
+ hide_label: document.getElementById('sel-yaxis-hide-label').checked,
1323
+ hide_spine: document.getElementById('sel-yaxis-hide-spine').checked
1324
+ };
1325
+ }
1326
+
1327
+ // Trigger update
1328
+ updatePreview();
1329
+ document.getElementById('status').textContent = `Applied changes to ${elementInfo.displayName}`;
1330
+ }
1331
+
1332
+ // Sync color inputs
1333
+ function setupColorSync(colorId, textId) {
1334
+ const colorInput = document.getElementById(colorId);
1335
+ const textInput = document.getElementById(textId);
1336
+ if (colorInput && textInput) {
1337
+ colorInput.addEventListener('input', () => {
1338
+ textInput.value = colorInput.value;
1339
+ });
1340
+ textInput.addEventListener('input', () => {
1341
+ if (/^#[0-9A-Fa-f]{6}$/.test(textInput.value)) {
1342
+ colorInput.value = textInput.value;
1343
+ }
1344
+ });
1345
+ }
1346
+ }
1347
+
1348
+ // Theme management
1349
+ function toggleTheme() {
1350
+ const html = document.documentElement;
1351
+ const current = html.getAttribute('data-theme');
1352
+ const next = current === 'dark' ? 'light' : 'dark';
1353
+ html.setAttribute('data-theme', next);
1354
+ document.getElementById('theme-icon').innerHTML = next === 'dark' ? '&#9790;' : '&#9788;';
1355
+ localStorage.setItem('scitex-editor-theme', next);
1356
+ }
1357
+
1358
+ // Collapsible sections
1359
+ function toggleSection(header) {
1360
+ header.classList.toggle('collapsed');
1361
+ const content = header.nextElementSibling;
1362
+ content.classList.toggle('collapsed');
1363
+ }
1364
+
1365
+ function switchAxisTab(axis) {
1366
+ // Update tab buttons
1367
+ document.querySelectorAll('.axis-tab').forEach(tab => {
1368
+ tab.classList.remove('active');
1369
+ });
1370
+ document.getElementById('axis-tab-' + axis).classList.add('active');
1371
+
1372
+ // Update panels
1373
+ document.querySelectorAll('.axis-panel').forEach(panel => {
1374
+ panel.style.display = 'none';
1375
+ });
1376
+ document.getElementById('axis-panel-' + axis).style.display = 'block';
1377
+ }
1378
+
1379
+ function toggleCustomLegendPosition() {
1380
+ const legendLoc = document.getElementById('legend_loc').value;
1381
+ const customCoordsDiv = document.getElementById('custom-legend-coords');
1382
+ customCoordsDiv.style.display = legendLoc === 'custom' ? 'flex' : 'none';
1383
+ }
1384
+
1385
+ // Dimension unit toggle
1386
+ function setDimensionUnit(unit) {
1387
+ if (unit === dimensionUnit) return;
1388
+
1389
+ const widthInput = document.getElementById('fig_width');
1390
+ const heightInput = document.getElementById('fig_height');
1391
+ const widthLabel = document.getElementById('fig_width_label');
1392
+ const heightLabel = document.getElementById('fig_height_label');
1393
+ const mmBtn = document.getElementById('unit-mm');
1394
+ const inchBtn = document.getElementById('unit-inch');
1395
+
1396
+ // Get current values
1397
+ let width = parseFloat(widthInput.value) || 0;
1398
+ let height = parseFloat(heightInput.value) || 0;
1399
+
1400
+ // Convert values
1401
+ if (unit === 'mm' && dimensionUnit === 'inch') {
1402
+ // inch to mm
1403
+ width = Math.round(width * INCH_TO_MM * 10) / 10;
1404
+ height = Math.round(height * INCH_TO_MM * 10) / 10;
1405
+ widthInput.min = 10;
1406
+ widthInput.max = 300;
1407
+ widthInput.step = 1;
1408
+ heightInput.min = 10;
1409
+ heightInput.max = 300;
1410
+ heightInput.step = 1;
1411
+ } else if (unit === 'inch' && dimensionUnit === 'mm') {
1412
+ // mm to inch
1413
+ width = Math.round(width * MM_TO_INCH * 100) / 100;
1414
+ height = Math.round(height * MM_TO_INCH * 100) / 100;
1415
+ widthInput.min = 0.5;
1416
+ widthInput.max = 12;
1417
+ widthInput.step = 0.05;
1418
+ heightInput.min = 0.5;
1419
+ heightInput.max = 12;
1420
+ heightInput.step = 0.05;
1421
+ }
1422
+
1423
+ // Update values and labels
1424
+ widthInput.value = width;
1425
+ heightInput.value = height;
1426
+ widthLabel.textContent = `Width (${unit})`;
1427
+ heightLabel.textContent = `Height (${unit})`;
1428
+
1429
+ // Update button states
1430
+ if (unit === 'mm') {
1431
+ mmBtn.classList.add('active');
1432
+ inchBtn.classList.remove('active');
1433
+ } else {
1434
+ mmBtn.classList.remove('active');
1435
+ inchBtn.classList.add('active');
1436
+ }
1437
+
1438
+ dimensionUnit = unit;
1439
+ }
1440
+
1441
+ // Background type management
1442
+ let backgroundType = 'transparent';
1443
+ let initializingBackground = true; // Flag to prevent updates during init
1444
+
1445
+ function setBackgroundType(type) {
1446
+ backgroundType = type;
1447
+
1448
+ // Update hidden inputs for collectOverrides
1449
+ const facecolorInput = document.getElementById('facecolor');
1450
+ const transparentInput = document.getElementById('transparent');
1451
+
1452
+ if (type === 'white') {
1453
+ facecolorInput.value = '#ffffff';
1454
+ transparentInput.value = 'false';
1455
+ } else if (type === 'black') {
1456
+ facecolorInput.value = '#000000';
1457
+ transparentInput.value = 'false';
1458
+ } else {
1459
+ // transparent
1460
+ facecolorInput.value = '#ffffff';
1461
+ transparentInput.value = 'true';
1462
+ }
1463
+
1464
+ // Update button states
1465
+ document.querySelectorAll('.bg-btn').forEach(btn => btn.classList.remove('active'));
1466
+ document.getElementById(`bg-${type}`).classList.add('active');
1467
+
1468
+ // Trigger update only after initialization
1469
+ if (!initializingBackground) {
1470
+ scheduleUpdate();
1471
+ }
1472
+ }
1473
+
1474
+ // Get figure dimensions in inches (for matplotlib)
1475
+ function getFigSizeInches() {
1476
+ let width = parseFloat(document.getElementById('fig_width').value) || 80;
1477
+ let height = parseFloat(document.getElementById('fig_height').value) || 68;
1478
+
1479
+ if (dimensionUnit === 'mm') {
1480
+ width = width * MM_TO_INCH;
1481
+ height = height * MM_TO_INCH;
1482
+ }
1483
+
1484
+ return [Math.round(width * 100) / 100, Math.round(height * 100) / 100];
1485
+ }
1486
+
1487
+ // Initialize fields
1488
+ document.addEventListener('DOMContentLoaded', () => {
1489
+ // Load saved theme
1490
+ const savedTheme = localStorage.getItem('scitex-editor-theme');
1491
+ if (savedTheme) {
1492
+ document.documentElement.setAttribute('data-theme', savedTheme);
1493
+ document.getElementById('theme-icon').innerHTML = savedTheme === 'dark' ? '&#9790;' : '&#9788;';
1494
+ }
1495
+
1496
+ // Labels - Title
1497
+ if (overrides.title) document.getElementById('title').value = overrides.title;
1498
+ document.getElementById('show_title').checked = overrides.show_title !== false;
1499
+ document.getElementById('title_fontsize').value = overrides.title_fontsize || 8;
1500
+ // Labels - Caption
1501
+ if (overrides.caption) document.getElementById('caption').value = overrides.caption;
1502
+ document.getElementById('show_caption').checked = overrides.show_caption || false;
1503
+ document.getElementById('caption_fontsize').value = overrides.caption_fontsize || 7;
1504
+ // Labels - Axis
1505
+ if (overrides.xlabel) document.getElementById('xlabel').value = overrides.xlabel;
1506
+ if (overrides.ylabel) document.getElementById('ylabel').value = overrides.ylabel;
1507
+
1508
+ // Axis limits
1509
+ if (overrides.xlim) {
1510
+ document.getElementById('xmin').value = overrides.xlim[0];
1511
+ document.getElementById('xmax').value = overrides.xlim[1];
1512
+ }
1513
+ if (overrides.ylim) {
1514
+ document.getElementById('ymin').value = overrides.ylim[0];
1515
+ document.getElementById('ymax').value = overrides.ylim[1];
1516
+ }
1517
+
1518
+ // Traces
1519
+ updateTracesList();
1520
+
1521
+ // Legend
1522
+ document.getElementById('legend_visible').checked = overrides.legend_visible !== false;
1523
+ document.getElementById('legend_loc').value = overrides.legend_loc || 'best';
1524
+ document.getElementById('legend_frameon').checked = overrides.legend_frameon || false;
1525
+ document.getElementById('legend_fontsize').value = overrides.legend_fontsize || 6;
1526
+ document.getElementById('legend_ncols').value = overrides.legend_ncols || 1;
1527
+ document.getElementById('legend_x').value = overrides.legend_x !== undefined ? overrides.legend_x : 0.95;
1528
+ document.getElementById('legend_y').value = overrides.legend_y !== undefined ? overrides.legend_y : 0.95;
1529
+ toggleCustomLegendPosition();
1530
+
1531
+ // Axis and Ticks - X Axis (Bottom)
1532
+ document.getElementById('x_n_ticks').value = overrides.x_n_ticks || overrides.n_ticks || 4;
1533
+ document.getElementById('hide_x_ticks').checked = overrides.hide_x_ticks || false;
1534
+ document.getElementById('x_tick_fontsize').value = overrides.x_tick_fontsize || overrides.tick_fontsize || 7;
1535
+ document.getElementById('x_tick_direction').value = overrides.x_tick_direction || overrides.tick_direction || 'out';
1536
+ document.getElementById('x_tick_length').value = overrides.x_tick_length || overrides.tick_length || 0.8;
1537
+ document.getElementById('x_tick_width').value = overrides.x_tick_width || overrides.tick_width || 0.2;
1538
+ // X Axis (Top)
1539
+ document.getElementById('show_x_top').checked = overrides.show_x_top || false;
1540
+ document.getElementById('x_top_mirror').checked = overrides.x_top_mirror || false;
1541
+ // Y Axis (Left)
1542
+ document.getElementById('y_n_ticks').value = overrides.y_n_ticks || overrides.n_ticks || 4;
1543
+ document.getElementById('hide_y_ticks').checked = overrides.hide_y_ticks || false;
1544
+ document.getElementById('y_tick_fontsize').value = overrides.y_tick_fontsize || overrides.tick_fontsize || 7;
1545
+ document.getElementById('y_tick_direction').value = overrides.y_tick_direction || overrides.tick_direction || 'out';
1546
+ document.getElementById('y_tick_length').value = overrides.y_tick_length || overrides.tick_length || 0.8;
1547
+ document.getElementById('y_tick_width').value = overrides.y_tick_width || overrides.tick_width || 0.2;
1548
+ // Y Axis (Right)
1549
+ document.getElementById('show_y_right').checked = overrides.show_y_right || false;
1550
+ document.getElementById('y_right_mirror').checked = overrides.y_right_mirror || false;
1551
+ // Spines
1552
+ document.getElementById('hide_bottom_spine').checked = overrides.hide_bottom_spine || false;
1553
+ document.getElementById('hide_left_spine').checked = overrides.hide_left_spine || false;
1554
+ // Z Axis (3D)
1555
+ document.getElementById('hide_z_ticks').checked = overrides.hide_z_ticks || false;
1556
+ document.getElementById('z_n_ticks').value = overrides.z_n_ticks || 4;
1557
+ document.getElementById('z_tick_fontsize').value = overrides.z_tick_fontsize || 7;
1558
+ document.getElementById('z_tick_direction').value = overrides.z_tick_direction || 'out';
1559
+
1560
+ // Style
1561
+ document.getElementById('grid').checked = overrides.grid || false;
1562
+ document.getElementById('hide_top_spine').checked = overrides.hide_top_spine !== false;
1563
+ document.getElementById('hide_right_spine').checked = overrides.hide_right_spine !== false;
1564
+ document.getElementById('axis_width').value = overrides.axis_width || 0.2;
1565
+ document.getElementById('axis_fontsize').value = overrides.axis_fontsize || 7;
1566
+ // Initialize background type from overrides
1567
+ const isTransparent = overrides.transparent !== false;
1568
+ const facecolor = overrides.facecolor || '#ffffff';
1569
+ document.getElementById('facecolor').value = facecolor;
1570
+
1571
+ if (isTransparent) {
1572
+ setBackgroundType('transparent');
1573
+ } else if (facecolor === '#000000') {
1574
+ setBackgroundType('black');
1575
+ } else {
1576
+ setBackgroundType('white');
1577
+ }
1578
+
1579
+ // Dimensions (convert from inches in metadata to mm by default)
1580
+ if (overrides.fig_size) {
1581
+ // fig_size is in inches in the JSON - convert to mm for default display
1582
+ const widthMm = Math.round(overrides.fig_size[0] * INCH_TO_MM);
1583
+ const heightMm = Math.round(overrides.fig_size[1] * INCH_TO_MM);
1584
+ document.getElementById('fig_width').value = widthMm;
1585
+ document.getElementById('fig_height').value = heightMm;
1586
+ }
1587
+ document.getElementById('dpi').value = overrides.dpi || 300;
1588
+ // Default unit is mm, which is already set in HTML and JS state
1589
+
1590
+ // Note: facecolor is now managed by background toggle buttons (white/transparent/black)
1591
+ // No text input sync needed
1592
+
1593
+ updateAnnotationsList();
1594
+ // NOTE: Don't call updatePreview() here - we want to use existing PNG/SVG
1595
+ // loadInitialPreview() will load the original file and then start auto-update
1596
+ initHoverSystem();
1597
+ refreshStats(); // Load statistical test results
1598
+
1599
+ // Setup color sync for selected element property inputs
1600
+ setupColorSync('sel-trace-color', 'sel-trace-color-text');
1601
+ setupColorSync('sel-scatter-color', 'sel-scatter-color-text');
1602
+ setupColorSync('sel-scatter-edgecolor', 'sel-scatter-edgecolor-text');
1603
+ setupColorSync('sel-fill-color', 'sel-fill-color-text');
1604
+ setupColorSync('sel-bar-facecolor', 'sel-bar-facecolor-text');
1605
+ setupColorSync('sel-bar-edgecolor', 'sel-bar-edgecolor-text');
1606
+ setupColorSync('sel-panel-facecolor', 'sel-panel-facecolor-text');
1607
+
1608
+ // Mark initialization complete - now background changes will trigger updates
1609
+ initializingBackground = false;
1610
+
1611
+ // Setup field-to-element synchronization (highlight element when field is focused)
1612
+ setupFieldToElementSync();
1613
+
1614
+ // Load initial preview from existing PNG/SVG (no re-render)
1615
+ loadInitialPreview();
1616
+
1617
+ // Add resize handler to update overlay when window/image size changes
1618
+ window.addEventListener('resize', () => {
1619
+ updateOverlay();
1620
+ });
1621
+
1622
+ // Use ResizeObserver to detect when the preview container changes size
1623
+ const previewContainer = document.getElementById('preview-container');
1624
+ if (previewContainer && typeof ResizeObserver !== 'undefined') {
1625
+ const resizeObserver = new ResizeObserver(() => {
1626
+ updateOverlay();
1627
+ });
1628
+ resizeObserver.observe(previewContainer);
1629
+ }
1630
+ });
1631
+
1632
+ // =============================================================================
1633
+ // Loading Helpers
1634
+ // =============================================================================
1635
+ function showLoading() {
1636
+ const overlay = document.getElementById('loading-overlay');
1637
+ if (overlay) overlay.style.display = 'flex';
1638
+ }
1639
+
1640
+ function hideLoading() {
1641
+ const overlay = document.getElementById('loading-overlay');
1642
+ if (overlay) overlay.style.display = 'none';
1643
+ }
1644
+
1645
+ // Update form controls from overrides (used when switching panels)
1646
+ function updateControlsFromOverrides() {
1647
+ console.log('updateControlsFromOverrides called');
1648
+ console.log('overrides.traces:', overrides.traces);
1649
+ console.log('traces variable:', traces);
1650
+
1651
+ // Update title - try both id and name selectors
1652
+ const titleInput = document.getElementById('title') || document.querySelector('input[name="title"]');
1653
+ if (titleInput) {
1654
+ // Try overrides.title first, then axes[0].title
1655
+ let title = overrides.title;
1656
+ if (title === undefined && overrides.axes && overrides.axes[0]) {
1657
+ title = overrides.axes[0].title || '';
1658
+ }
1659
+ titleInput.value = title || '';
1660
+ }
1661
+
1662
+ // Update figure size
1663
+ const widthInput = document.getElementById('fig-width');
1664
+ const heightInput = document.getElementById('fig-height');
1665
+ if (widthInput && overrides.figure_width !== undefined) {
1666
+ widthInput.value = overrides.figure_width;
1667
+ }
1668
+ if (heightInput && overrides.figure_height !== undefined) {
1669
+ heightInput.value = overrides.figure_height;
1670
+ }
1671
+
1672
+ // Update xlabel, ylabel - try overrides first, then axes[0]
1673
+ const xlabelInput = document.getElementById('xlabel');
1674
+ const ylabelInput = document.getElementById('ylabel');
1675
+ let xlabel = overrides.xlabel;
1676
+ let ylabel = overrides.ylabel;
1677
+ if (overrides.axes && overrides.axes[0]) {
1678
+ xlabel = xlabel || overrides.axes[0].xlabel || '';
1679
+ ylabel = ylabel || overrides.axes[0].ylabel || '';
1680
+ }
1681
+ if (xlabelInput) xlabelInput.value = xlabel || '';
1682
+ if (ylabelInput) ylabelInput.value = ylabel || '';
1683
+
1684
+ // Update traces list
1685
+ updateTracesList();
1686
+ }
1687
+
1688
+ // Load preview from existing file without re-rendering
1689
+ async function loadInitialPreview() {
1690
+ setStatus('Loading preview...', false);
1691
+ try {
1692
+ const darkMode = isDarkMode();
1693
+ const resp = await fetch(`/preview?dark_mode=${darkMode}`);
1694
+ const data = await resp.json();
1695
+
1696
+ console.log('=== PREVIEW DATA RECEIVED ===');
1697
+ console.log('format:', data.format);
1698
+ console.log('img_size:', data.img_size);
1699
+ console.log('bboxes keys:', Object.keys(data.bboxes || {}));
1700
+ console.log('bboxes:', JSON.stringify(data.bboxes, null, 2));
1701
+
1702
+ const previewContainer = document.getElementById('preview-container');
1703
+ const img = document.getElementById('preview-img');
1704
+
1705
+ if (data.format === 'svg' && data.svg) {
1706
+ // Handle SVG: replace img with inline SVG
1707
+ const svgWrapper = document.createElement('div');
1708
+ svgWrapper.id = 'preview-svg-wrapper';
1709
+ svgWrapper.innerHTML = data.svg;
1710
+ svgWrapper.style.width = '100%';
1711
+ svgWrapper.style.maxHeight = '70vh';
1712
+
1713
+ // Find the SVG element and set styles
1714
+ const svgEl = svgWrapper.querySelector('svg');
1715
+ if (svgEl) {
1716
+ svgEl.style.width = '100%';
1717
+ svgEl.style.height = 'auto';
1718
+ svgEl.style.maxHeight = '70vh';
1719
+ svgEl.id = 'preview-img'; // Keep same ID for event handlers
1720
+ }
1721
+
1722
+ img.style.display = 'none';
1723
+ const existingWrapper = document.getElementById('preview-svg-wrapper');
1724
+ if (existingWrapper) existingWrapper.remove();
1725
+ previewContainer.appendChild(svgWrapper);
1726
+ } else if (data.image) {
1727
+ // Handle PNG: show as base64 image
1728
+ img.src = 'data:image/png;base64,' + data.image;
1729
+ img.style.display = 'block';
1730
+ const existingWrapper = document.getElementById('preview-svg-wrapper');
1731
+ if (existingWrapper) existingWrapper.remove();
1732
+ }
1733
+
1734
+ if (data.bboxes) {
1735
+ elementBboxes = data.bboxes;
1736
+ originalBboxes = JSON.parse(JSON.stringify(data.bboxes)); // Deep copy
1737
+ if (data.bboxes._meta) {
1738
+ schemaMeta = data.bboxes._meta;
1739
+ }
1740
+ console.log('Loaded bboxes:', Object.keys(elementBboxes).filter(k => k !== '_meta'));
1741
+ }
1742
+ if (data.img_size) {
1743
+ imgSize = data.img_size;
1744
+ originalImgSize = {...data.img_size}; // Copy
1745
+ console.log('Loaded imgSize:', imgSize);
1746
+ }
1747
+
1748
+ isShowingOriginalPreview = true;
1749
+ updateOverlay();
1750
+ setStatus('Preview loaded', false);
1751
+
1752
+ // Initialize hover system for the SVG if needed
1753
+ if (data.format === 'svg') {
1754
+ const svgWrapper = document.getElementById('preview-svg-wrapper');
1755
+ if (svgWrapper) {
1756
+ initHoverSystemForElement(svgWrapper.querySelector('svg'));
1757
+ }
1758
+ }
1759
+
1760
+ // Draw debug bboxes if debug mode is on
1761
+ if (debugMode) {
1762
+ drawDebugBboxes();
1763
+ }
1764
+
1765
+ // Handle multi-panel figz bundles
1766
+ if (data.panel_info && data.panel_info.panels) {
1767
+ panelData = data.panel_info;
1768
+ currentPanelIndex = data.panel_info.current_index || 0;
1769
+ console.log('Multi-panel figz detected:', panelData.panels.length, 'panels');
1770
+ loadPanelGrid();
1771
+ }
1772
+
1773
+ // Start auto-update AFTER initial preview is loaded
1774
+ setAutoUpdateInterval();
1775
+ } catch (e) {
1776
+ setStatus('Error loading preview: ' + e.message, true);
1777
+ console.error('Preview load error:', e);
1778
+ // Start auto-update even on error so the editor works
1779
+ setAutoUpdateInterval();
1780
+ }
1781
+ }
1782
+
1783
+ // =============================================================================
1784
+ // Multi-Panel Navigation
1785
+ // =============================================================================
1786
+ async function loadPanelGrid() {
1787
+ if (!panelData || panelData.panels.length <= 1) {
1788
+ // Not a multi-panel bundle or only one panel
1789
+ document.getElementById('panel-grid-section').style.display = 'none';
1790
+ document.getElementById('preview-header').style.display = 'none';
1791
+ return;
1792
+ }
1793
+
1794
+ console.log('Loading panel canvas for', panelData.panels.length, 'panels');
1795
+
1796
+ // Show panel header
1797
+ document.getElementById('preview-header').style.display = 'flex';
1798
+
1799
+ // Fetch all panel images with bboxes
1800
+ try {
1801
+ const resp = await fetch('/panels');
1802
+ const data = await resp.json();
1803
+
1804
+ if (data.error) {
1805
+ console.error('Panel canvas error:', data.error);
1806
+ return;
1807
+ }
1808
+
1809
+ const canvasEl = document.getElementById('panel-canvas');
1810
+ canvasEl.innerHTML = '';
1811
+
1812
+ // Calculate layout - arrange panels in a grid-like canvas
1813
+ const numPanels = data.panels.length;
1814
+ const cols = Math.ceil(Math.sqrt(numPanels));
1815
+ const baseWidth = 220;
1816
+ const baseHeight = 180;
1817
+ const padding = 15;
1818
+
1819
+ data.panels.forEach((panel, idx) => {
1820
+ // Store bboxes and imgSize in cache for interactive hover/click
1821
+ if (panel.bboxes && panel.img_size) {
1822
+ panelBboxesCache[panel.name] = {
1823
+ bboxes: panel.bboxes,
1824
+ imgSize: panel.img_size
1825
+ };
1826
+ console.log(`Panel ${panel.name}: ${Object.keys(panel.bboxes).filter(k => k !== '_meta').length} bboxes, img: ${panel.img_size.width}x${panel.img_size.height}`);
1827
+ } else {
1828
+ console.warn(`Panel ${panel.name}: missing bboxes or img_size`, {bboxes: !!panel.bboxes, img_size: !!panel.img_size});
1829
+ }
1830
+
1831
+ // Calculate position
1832
+ const col = idx % cols;
1833
+ const row = Math.floor(idx / cols);
1834
+ if (!panelPositions[panel.name]) {
1835
+ panelPositions[panel.name] = {
1836
+ x: padding + col * (baseWidth + padding),
1837
+ y: padding + row * (baseHeight + padding),
1838
+ width: baseWidth,
1839
+ height: baseHeight,
1840
+ };
1841
+ }
1842
+ const pos = panelPositions[panel.name];
1843
+
1844
+ const item = document.createElement('div');
1845
+ item.className = 'panel-canvas-item' + (idx === currentPanelIndex ? ' active' : '');
1846
+ item.dataset.panelIndex = idx;
1847
+ item.dataset.panelName = panel.name;
1848
+ item.style.left = pos.x + 'px';
1849
+ item.style.top = pos.y + 'px';
1850
+ item.style.width = pos.width + 'px';
1851
+ item.style.height = pos.height + 'px';
1852
+
1853
+ if (panel.image) {
1854
+ item.innerHTML = `
1855
+ <span class="panel-canvas-label">Panel ${panel.name}</span>
1856
+ <div class="panel-card-container">
1857
+ <img src="data:image/png;base64,${panel.image}" alt="Panel ${panel.name}">
1858
+ <svg class="panel-card-overlay" id="panel-overlay-${idx}"></svg>
1859
+ </div>
1860
+ <div class="panel-canvas-resize"></div>
1861
+ `;
1862
+ } else {
1863
+ item.innerHTML = `
1864
+ <span class="panel-canvas-label">Panel ${panel.name}</span>
1865
+ <div style="padding: 20px; color: var(--text-muted);">No preview</div>
1866
+ `;
1867
+ }
1868
+
1869
+ // Add interactive event handlers
1870
+ initCanvasItemInteraction(item, idx, panel.name);
1871
+
1872
+ canvasEl.appendChild(item);
1873
+ });
1874
+
1875
+ // Update canvas height to fit all panels
1876
+ const maxY = Math.max(...Object.values(panelPositions).map(p => p.y + p.height)) + padding;
1877
+ canvasEl.style.minHeight = Math.max(400, maxY) + 'px';
1878
+
1879
+ // Update panel indicator
1880
+ updatePanelIndicator();
1881
+
1882
+ // Show canvas for multi-panel figures
1883
+ if (data.panels.length > 1) {
1884
+ showingPanelGrid = true;
1885
+ document.getElementById('panel-grid-section').style.display = 'block';
1886
+ // Hide single-panel preview for multi-panel bundles
1887
+ const previewWrapper = document.querySelector('.preview-wrapper');
1888
+ if (previewWrapper) {
1889
+ previewWrapper.style.display = 'none';
1890
+ }
1891
+ }
1892
+ } catch (e) {
1893
+ console.error('Error loading panels:', e);
1894
+ }
1895
+ }
1896
+
1897
+ // Initialize interactive handlers for canvas panel items
1898
+ function initCanvasItemInteraction(item, panelIdx, panelName) {
1899
+ const container = item.querySelector('.panel-card-container');
1900
+ if (!container) return;
1901
+
1902
+ const img = container.querySelector('img');
1903
+ const overlay = container.querySelector('svg');
1904
+ if (!img || !overlay) return;
1905
+
1906
+ // Wait for image to load to get dimensions
1907
+ img.addEventListener('load', () => {
1908
+ overlay.setAttribute('width', img.offsetWidth);
1909
+ overlay.setAttribute('height', img.offsetHeight);
1910
+ overlay.style.width = img.offsetWidth + 'px';
1911
+ overlay.style.height = img.offsetHeight + 'px';
1912
+ });
1913
+
1914
+ // Mousemove for hover detection
1915
+ container.addEventListener('mousemove', (e) => {
1916
+ const panelCache = panelBboxesCache[panelName];
1917
+ if (!panelCache) return;
1918
+
1919
+ const rect = img.getBoundingClientRect();
1920
+ const x = e.clientX - rect.left;
1921
+ const y = e.clientY - rect.top;
1922
+
1923
+ const scaleX = panelCache.imgSize.width / rect.width;
1924
+ const scaleY = panelCache.imgSize.height / rect.height;
1925
+ const imgX = x * scaleX;
1926
+ const imgY = y * scaleY;
1927
+
1928
+ const element = findElementInPanelAt(imgX, imgY, panelCache.bboxes);
1929
+ if (element !== panelHoveredElement || activePanelCard !== item) {
1930
+ panelHoveredElement = element;
1931
+ activePanelCard = item;
1932
+ updatePanelOverlay(overlay, panelCache.bboxes, panelCache.imgSize, rect.width, rect.height, panelHoveredElement, null);
1933
+ }
1934
+ });
1935
+
1936
+ // Mouseleave to clear hover
1937
+ container.addEventListener('mouseleave', () => {
1938
+ panelHoveredElement = null;
1939
+ if (activePanelCard === item) {
1940
+ updatePanelOverlay(overlay, {}, {width: 0, height: 0}, 0, 0, null, null);
1941
+ }
1942
+ });
1943
+
1944
+ // Click to select element
1945
+ container.addEventListener('click', (e) => {
1946
+ e.stopPropagation();
1947
+
1948
+ // Recalculate element at click position (in case hover didn't detect it)
1949
+ const panelCache = panelBboxesCache[panelName];
1950
+ let clickedElement = panelHoveredElement;
1951
+
1952
+ if (panelCache && img) {
1953
+ const rect = img.getBoundingClientRect();
1954
+ const x = e.clientX - rect.left;
1955
+ const y = e.clientY - rect.top;
1956
+
1957
+ const scaleX = panelCache.imgSize.width / rect.width;
1958
+ const scaleY = panelCache.imgSize.height / rect.height;
1959
+ const imgX = x * scaleX;
1960
+ const imgY = y * scaleY;
1961
+
1962
+ clickedElement = findElementInPanelAt(imgX, imgY, panelCache.bboxes);
1963
+ console.log(`Click at (${imgX.toFixed(0)}, ${imgY.toFixed(0)}) -> element: ${clickedElement}`);
1964
+ }
1965
+
1966
+ if (clickedElement) {
1967
+ document.querySelectorAll('.panel-canvas-item').forEach((c, i) => {
1968
+ c.classList.toggle('active', i === panelIdx);
1969
+ });
1970
+
1971
+ // If already on this panel, just update selection without server call
1972
+ if (currentPanelIndex === panelIdx && panelCache) {
1973
+ console.log(`Same panel (${panelIdx}), updating selection to: ${clickedElement}`);
1974
+ selectedElement = clickedElement;
1975
+ // Sync elementBboxes with panel cache bboxes
1976
+ elementBboxes = panelCache.bboxes || {};
1977
+ imgSize = panelCache.imgSize || imgSize;
1978
+ console.log('elementBboxes keys:', Object.keys(elementBboxes));
1979
+ updateOverlay();
1980
+ console.log('Calling scrollToSection with:', selectedElement);
1981
+ scrollToSection(selectedElement);
1982
+ setStatus(`Selected: ${clickedElement}`, false);
1983
+ } else {
1984
+ currentPanelIndex = panelIdx;
1985
+ loadPanelForEditing(panelIdx, panelName, clickedElement);
1986
+ }
1987
+ } else {
1988
+ console.log(`No element found, selecting panel ${panelName}`);
1989
+ selectPanel(panelIdx);
1990
+ }
1991
+ });
1992
+
1993
+ // Drag support for repositioning
1994
+ item.addEventListener('mousedown', (e) => {
1995
+ if (e.target.classList.contains('panel-canvas-resize')) {
1996
+ startResize(e, item, panelName);
1997
+ } else if (!e.target.closest('.panel-card-container')) {
1998
+ startDrag(e, item, panelName);
1999
+ }
2000
+ });
2001
+ }
2002
+
2003
+ // Initialize interactive hover/click handlers for a panel card
2004
+ function initPanelCardInteraction(card, panelIdx, panelName) {
2005
+ const container = card.querySelector('.panel-card-container');
2006
+ if (!container) return;
2007
+
2008
+ const img = container.querySelector('img');
2009
+ const overlay = container.querySelector('svg');
2010
+ if (!img || !overlay) return;
2011
+
2012
+ // Wait for image to load to get dimensions
2013
+ img.addEventListener('load', () => {
2014
+ overlay.setAttribute('width', img.offsetWidth);
2015
+ overlay.setAttribute('height', img.offsetHeight);
2016
+ overlay.style.width = img.offsetWidth + 'px';
2017
+ overlay.style.height = img.offsetHeight + 'px';
2018
+ });
2019
+
2020
+ // Mousemove for hover detection
2021
+ container.addEventListener('mousemove', (e) => {
2022
+ const panelCache = panelBboxesCache[panelName];
2023
+ if (!panelCache) return;
2024
+
2025
+ const rect = img.getBoundingClientRect();
2026
+ const x = e.clientX - rect.left;
2027
+ const y = e.clientY - rect.top;
2028
+
2029
+ const scaleX = panelCache.imgSize.width / rect.width;
2030
+ const scaleY = panelCache.imgSize.height / rect.height;
2031
+ const imgX = x * scaleX;
2032
+ const imgY = y * scaleY;
2033
+
2034
+ // Find element at cursor using panel's bboxes
2035
+ const element = findElementInPanelAt(imgX, imgY, panelCache.bboxes);
2036
+ if (element !== panelHoveredElement || activePanelCard !== card) {
2037
+ panelHoveredElement = element;
2038
+ activePanelCard = card;
2039
+ updatePanelOverlay(overlay, panelCache.bboxes, panelCache.imgSize, rect.width, rect.height, panelHoveredElement, null);
2040
+ }
2041
+ });
2042
+
2043
+ // Mouseleave to clear hover
2044
+ container.addEventListener('mouseleave', () => {
2045
+ panelHoveredElement = null;
2046
+ if (activePanelCard === card) {
2047
+ updatePanelOverlay(overlay, {}, {width: 0, height: 0}, 0, 0, null, null);
2048
+ }
2049
+ });
2050
+
2051
+ // Click to select element
2052
+ container.addEventListener('click', (e) => {
2053
+ e.stopPropagation(); // Prevent card click from triggering selectPanel
2054
+
2055
+ if (panelHoveredElement) {
2056
+ // Set this panel as current and select the element
2057
+ currentPanelIndex = panelIdx;
2058
+
2059
+ // Update active state in grid
2060
+ document.querySelectorAll('.panel-card').forEach((c, i) => {
2061
+ c.classList.toggle('active', i === panelIdx);
2062
+ });
2063
+
2064
+ // Load this panel's data into the main editor
2065
+ loadPanelForEditing(panelIdx, panelName, panelHoveredElement);
2066
+ } else {
2067
+ // No element hovered, select the panel itself
2068
+ selectPanel(panelIdx);
2069
+ }
2070
+ });
2071
+ }
2072
+
2073
+ // Find element at position within a panel's bboxes
2074
+ function findElementInPanelAt(x, y, bboxes) {
2075
+ const PROXIMITY_THRESHOLD = 15;
2076
+ const SCATTER_THRESHOLD = 20;
2077
+
2078
+ let closestDataElement = null;
2079
+ let minDistance = Infinity;
2080
+
2081
+ for (const [name, bbox] of Object.entries(bboxes)) {
2082
+ if (name === '_meta') continue;
2083
+
2084
+ // Check data elements with points
2085
+ if (bbox.points && bbox.points.length > 0) {
2086
+ if (x >= bbox.x0 - SCATTER_THRESHOLD && x <= bbox.x1 + SCATTER_THRESHOLD &&
2087
+ y >= bbox.y0 - SCATTER_THRESHOLD && y <= bbox.y1 + SCATTER_THRESHOLD) {
2088
+
2089
+ const elementType = bbox.element_type || 'line';
2090
+ let dist;
2091
+
2092
+ if (elementType === 'scatter') {
2093
+ dist = distanceToNearestPoint(x, y, bbox.points);
2094
+ } else {
2095
+ dist = distanceToLine(x, y, bbox.points);
2096
+ }
2097
+
2098
+ if (dist < minDistance) {
2099
+ minDistance = dist;
2100
+ closestDataElement = name;
2101
+ }
2102
+ }
2103
+ }
2104
+ }
2105
+
2106
+ if (closestDataElement) {
2107
+ const bbox = bboxes[closestDataElement];
2108
+ const threshold = (bbox.element_type === 'scatter') ? SCATTER_THRESHOLD : PROXIMITY_THRESHOLD;
2109
+ if (minDistance <= threshold) {
2110
+ return closestDataElement;
2111
+ }
2112
+ }
2113
+
2114
+ // Check bbox containment for other elements
2115
+ const elementMatches = [];
2116
+ const panelMatches = [];
2117
+
2118
+ for (const [name, bbox] of Object.entries(bboxes)) {
2119
+ if (name === '_meta') continue;
2120
+ if (x >= bbox.x0 && x <= bbox.x1 && y >= bbox.y0 && y <= bbox.y1) {
2121
+ const area = (bbox.x1 - bbox.x0) * (bbox.y1 - bbox.y0);
2122
+ const isPanel = bbox.is_panel || name.endsWith('_panel');
2123
+ const hasPoints = bbox.points && bbox.points.length > 0;
2124
+
2125
+ if (hasPoints) continue;
2126
+ else if (isPanel) panelMatches.push({name, area, bbox});
2127
+ else elementMatches.push({name, area, bbox});
2128
+ }
2129
+ }
2130
+
2131
+ if (elementMatches.length > 0) {
2132
+ elementMatches.sort((a, b) => a.area - b.area);
2133
+ return elementMatches[0].name;
2134
+ }
2135
+
2136
+ if (panelMatches.length > 0) {
2137
+ panelMatches.sort((a, b) => a.area - b.area);
2138
+ return panelMatches[0].name;
2139
+ }
2140
+
2141
+ return null;
2142
+ }
2143
+
2144
+ // Toggle debug mode for panel grid
2145
+ function togglePanelDebugMode() {
2146
+ panelDebugMode = !panelDebugMode;
2147
+ const btn = document.getElementById('panel-debug-btn');
2148
+ if (btn) {
2149
+ btn.classList.toggle('active', panelDebugMode);
2150
+ btn.textContent = panelDebugMode ? 'Hide Hit Regions' : 'Show Hit Regions';
2151
+ }
2152
+ console.log('Panel debug mode:', panelDebugMode ? 'ON' : 'OFF');
2153
+
2154
+ // Redraw all panel overlays
2155
+ redrawAllPanelOverlays();
2156
+ }
2157
+
2158
+ // Redraw all panel overlays (useful for debug mode toggle)
2159
+ function redrawAllPanelOverlays() {
2160
+ document.querySelectorAll('.panel-canvas-item').forEach((item) => {
2161
+ const panelName = item.dataset.panelName;
2162
+ const panelCache = panelBboxesCache[panelName];
2163
+ if (!panelCache) {
2164
+ console.log('No cache for panel:', panelName);
2165
+ return;
2166
+ }
2167
+
2168
+ const container = item.querySelector('.panel-card-container');
2169
+ if (!container) return;
2170
+
2171
+ const img = container.querySelector('img');
2172
+ const overlay = container.querySelector('svg');
2173
+ if (!img || !overlay) return;
2174
+
2175
+ const rect = img.getBoundingClientRect();
2176
+ console.log(`Redraw panel ${panelName}: rect=${rect.width}x${rect.height}, bboxes=${Object.keys(panelCache.bboxes).length}`);
2177
+ if (rect.width > 0 && rect.height > 0) {
2178
+ updatePanelOverlay(overlay, panelCache.bboxes, panelCache.imgSize, rect.width, rect.height, null, null);
2179
+ }
2180
+ });
2181
+ }
2182
+
2183
+ // Update SVG overlay for a panel card
2184
+ function updatePanelOverlay(overlay, bboxes, imgSizePanel, displayWidth, displayHeight, hovered, selected) {
2185
+ if (!overlay || displayWidth === 0 || displayHeight === 0 || !imgSizePanel || imgSizePanel.width === 0) {
2186
+ if (overlay) overlay.innerHTML = '';
2187
+ return;
2188
+ }
2189
+
2190
+ overlay.setAttribute('width', displayWidth);
2191
+ overlay.setAttribute('height', displayHeight);
2192
+
2193
+ const scaleX = displayWidth / imgSizePanel.width;
2194
+ const scaleY = displayHeight / imgSizePanel.height;
2195
+
2196
+ let svg = '';
2197
+
2198
+ // Debug mode: draw all bboxes
2199
+ if (panelDebugMode && bboxes) {
2200
+ svg += drawPanelDebugBboxes(bboxes, scaleX, scaleY);
2201
+ }
2202
+
2203
+ function drawPanelElement(elementName, type) {
2204
+ const bbox = bboxes[elementName];
2205
+ if (!bbox) return '';
2206
+
2207
+ const elementType = bbox.element_type || '';
2208
+ const hasPoints = bbox.points && bbox.points.length > 0;
2209
+
2210
+ // Lines - draw as path
2211
+ if ((elementType === 'line' || elementName.includes('trace_')) && hasPoints) {
2212
+ if (bbox.points.length < 2) return '';
2213
+ const points = bbox.points.filter(pt => Array.isArray(pt) && pt.length >= 2);
2214
+ if (points.length < 2) return '';
2215
+
2216
+ let pathD = `M ${points[0][0] * scaleX} ${points[0][1] * scaleY}`;
2217
+ for (let i = 1; i < points.length; i++) {
2218
+ pathD += ` L ${points[i][0] * scaleX} ${points[i][1] * scaleY}`;
2219
+ }
2220
+
2221
+ const className = type === 'hover' ? 'hover-path' : 'selected-path';
2222
+ return `<path class="${className}" d="${pathD}"/>`;
2223
+ }
2224
+ // Scatter - draw as circles
2225
+ else if (elementType === 'scatter' && hasPoints) {
2226
+ const className = type === 'hover' ? 'hover-scatter' : 'selected-scatter';
2227
+ let result = '';
2228
+ for (const pt of bbox.points) {
2229
+ if (!Array.isArray(pt) || pt.length < 2) continue;
2230
+ result += `<circle class="${className}" cx="${pt[0] * scaleX}" cy="${pt[1] * scaleY}" r="3"/>`;
2231
+ }
2232
+ return result;
2233
+ }
2234
+ // Default - draw bbox rectangle
2235
+ else {
2236
+ const rectClass = type === 'hover' ? 'hover-rect' : 'selected-rect';
2237
+ const x = bbox.x0 * scaleX - 1;
2238
+ const y = bbox.y0 * scaleY - 1;
2239
+ const w = (bbox.x1 - bbox.x0) * scaleX + 2;
2240
+ const h = (bbox.y1 - bbox.y0) * scaleY + 2;
2241
+ return `<rect class="${rectClass}" x="${x}" y="${y}" width="${w}" height="${h}" rx="2"/>`;
2242
+ }
2243
+ }
2244
+
2245
+ if (hovered && hovered !== selected) {
2246
+ svg += drawPanelElement(hovered, 'hover');
2247
+ }
2248
+
2249
+ if (selected) {
2250
+ svg += drawPanelElement(selected, 'selected');
2251
+ }
2252
+
2253
+ overlay.innerHTML = svg;
2254
+ }
2255
+
2256
+ // Draw all bboxes for a panel in debug mode
2257
+ function drawPanelDebugBboxes(bboxes, scaleX, scaleY) {
2258
+ let svg = '';
2259
+ let count = 0;
2260
+
2261
+ for (const [name, bbox] of Object.entries(bboxes)) {
2262
+ if (name === '_meta') continue;
2263
+ if (bbox.x0 === undefined || bbox.y0 === undefined) continue;
2264
+
2265
+ count++;
2266
+ const hasPoints = bbox.points && bbox.points.length > 0;
2267
+ const elementType = bbox.element_type || '';
2268
+
2269
+ // Choose color based on element type
2270
+ let rectClass = 'debug-rect';
2271
+ if (name.includes('trace_') || elementType === 'line') {
2272
+ rectClass = 'debug-rect-trace';
2273
+ } else if (name.includes('legend')) {
2274
+ rectClass = 'debug-rect-legend';
2275
+ } else if (elementType === 'scatter') {
2276
+ rectClass = 'debug-rect-trace';
2277
+ }
2278
+
2279
+ // Draw bbox rectangle
2280
+ const x = bbox.x0 * scaleX;
2281
+ const y = bbox.y0 * scaleY;
2282
+ const w = (bbox.x1 - bbox.x0) * scaleX;
2283
+ const h = (bbox.y1 - bbox.y0) * scaleY;
2284
+
2285
+ svg += `<rect class="${rectClass}" x="${x}" y="${y}" width="${w}" height="${h}"/>`;
2286
+
2287
+ // Draw short label (truncated for small panels)
2288
+ const shortName = name.length > 10 ? name.substring(0, 8) + '..' : name;
2289
+ svg += `<text class="debug-label" x="${x + 1}" y="${y + 8}" style="font-size: 6px;">${shortName}</text>`;
2290
+
2291
+ // Draw path points if available
2292
+ if (hasPoints && bbox.points.length > 1) {
2293
+ let pathD = `M ${bbox.points[0][0] * scaleX} ${bbox.points[0][1] * scaleY}`;
2294
+ for (let i = 1; i < bbox.points.length; i++) {
2295
+ const pt = bbox.points[i];
2296
+ if (pt && pt.length >= 2) {
2297
+ pathD += ` L ${pt[0] * scaleX} ${pt[1] * scaleY}`;
2298
+ }
2299
+ }
2300
+ svg += `<path class="debug-path" d="${pathD}"/>`;
2301
+ }
2302
+ }
2303
+
2304
+ console.log(`Panel debug: ${count} elements with bboxes`);
2305
+ return svg;
2306
+ }
2307
+
2308
+ // Load panel data and switch to it for editing with a pre-selected element
2309
+ async function loadPanelForEditing(panelIdx, panelName, elementToSelect) {
2310
+ showLoading();
2311
+ setStatus(`Loading Panel ${panelName}...`, false);
2312
+
2313
+ try {
2314
+ const resp = await fetch(`/switch_panel/${panelIdx}`);
2315
+ const data = await resp.json();
2316
+
2317
+ if (data.error) {
2318
+ console.error('switch_panel error:', data.error);
2319
+ if (data.traceback) {
2320
+ console.error('Traceback:', data.traceback);
2321
+ }
2322
+ setStatus('Error: ' + data.error, true);
2323
+ hideLoading();
2324
+ return;
2325
+ }
2326
+
2327
+ // Update panel state
2328
+ currentPanelIndex = panelIdx;
2329
+ panelData.current_index = panelIdx;
2330
+ updatePanelIndicator();
2331
+
2332
+ // Update preview image
2333
+ const img = document.getElementById('preview-img');
2334
+ img.src = 'data:image/png;base64,' + data.image;
2335
+
2336
+ // Update bboxes and overlays
2337
+ elementBboxes = data.bboxes || {};
2338
+ if (data.img_size) {
2339
+ imgSize = data.img_size;
2340
+ }
2341
+
2342
+ // Update overrides
2343
+ if (data.overrides) {
2344
+ overrides = data.overrides;
2345
+ traces = overrides.traces || [];
2346
+ updateControlsFromOverrides();
2347
+ }
2348
+
2349
+ // Select the element that was clicked
2350
+ selectedElement = elementToSelect;
2351
+ updateOverlay();
2352
+
2353
+ // Scroll to section and show properties
2354
+ scrollToSection(selectedElement);
2355
+
2356
+ // Show single-panel preview when element selected
2357
+ const previewWrapper = document.querySelector('.preview-wrapper');
2358
+ if (previewWrapper) {
2359
+ previewWrapper.style.display = 'block';
2360
+ }
2361
+
2362
+ // Update panel path display in right panel header
2363
+ const panelPathEl = document.getElementById('panel-path-display');
2364
+ if (panelPathEl) {
2365
+ panelPathEl.textContent = `Panel: ${panelName}.pltz.d/spec.json`;
2366
+ }
2367
+
2368
+ setStatus(`Selected: ${elementToSelect} in Panel ${panelName}`, false);
2369
+ } catch (e) {
2370
+ setStatus('Error: ' + e.message, true);
2371
+ console.error('Panel load error:', e);
2372
+ } finally {
2373
+ hideLoading();
2374
+ }
2375
+ }
2376
+
2377
+ function togglePanelGrid() {
2378
+ showingPanelGrid = !showingPanelGrid;
2379
+ const gridSection = document.getElementById('panel-grid-section');
2380
+ const showBtn = document.getElementById('show-grid-btn');
2381
+
2382
+ if (showingPanelGrid) {
2383
+ gridSection.style.display = 'block';
2384
+ showBtn.textContent = 'Hide All';
2385
+ } else {
2386
+ gridSection.style.display = 'none';
2387
+ showBtn.textContent = 'Show All';
2388
+ }
2389
+ }
2390
+
2391
+ async function selectPanel(idx) {
2392
+ if (!panelData || idx < 0 || idx >= panelData.panels.length) return;
2393
+
2394
+ // Show loading state
2395
+ showLoading();
2396
+ setStatus('Switching panel...', false);
2397
+
2398
+ try {
2399
+ const resp = await fetch(`/switch_panel/${idx}`);
2400
+ const data = await resp.json();
2401
+
2402
+ if (data.error) {
2403
+ setStatus('Error: ' + data.error, true);
2404
+ hideLoading();
2405
+ return;
2406
+ }
2407
+
2408
+ // Update panel state
2409
+ currentPanelIndex = idx;
2410
+ panelData.current_index = idx;
2411
+ updatePanelIndicator();
2412
+
2413
+ // Update active state in grid
2414
+ document.querySelectorAll('.panel-card').forEach((card, i) => {
2415
+ card.classList.toggle('active', i === idx);
2416
+ });
2417
+
2418
+ // Update preview image
2419
+ const img = document.getElementById('preview-img');
2420
+ img.src = 'data:image/png;base64,' + data.image;
2421
+
2422
+ // Update bboxes and overlays
2423
+ elementBboxes = data.bboxes || {};
2424
+ if (data.img_size) {
2425
+ imgSize = data.img_size;
2426
+ }
2427
+
2428
+ // Update overrides
2429
+ if (data.overrides) {
2430
+ overrides = data.overrides;
2431
+ traces = overrides.traces || [];
2432
+ updateControlsFromOverrides();
2433
+ }
2434
+
2435
+ updateOverlay();
2436
+ if (debugMode) {
2437
+ drawDebugBboxes();
2438
+ }
2439
+
2440
+ // Update panel path display in right panel header
2441
+ const panelPathEl = document.getElementById('panel-path-display');
2442
+ if (panelPathEl && data.panel_name) {
2443
+ panelPathEl.textContent = `Panel: ${data.panel_name}/spec.json`;
2444
+ }
2445
+
2446
+ setStatus(`Switched to Panel ${data.panel_name.replace('.pltz.d', '')}`, false);
2447
+ } catch (e) {
2448
+ setStatus('Error switching panel: ' + e.message, true);
2449
+ console.error('Panel switch error:', e);
2450
+ } finally {
2451
+ hideLoading();
2452
+ }
2453
+ }
2454
+
2455
+ function prevPanel() {
2456
+ if (panelData && currentPanelIndex > 0) {
2457
+ selectPanel(currentPanelIndex - 1);
2458
+ }
2459
+ }
2460
+
2461
+ function nextPanel() {
2462
+ if (panelData && currentPanelIndex < panelData.panels.length - 1) {
2463
+ selectPanel(currentPanelIndex + 1);
2464
+ }
2465
+ }
2466
+
2467
+ function updatePanelIndicator() {
2468
+ if (!panelData) return;
2469
+
2470
+ const total = panelData.panels.length;
2471
+ const current = currentPanelIndex + 1;
2472
+ const panelName = panelData.panels[currentPanelIndex];
2473
+
2474
+ document.getElementById('panel-indicator').textContent = `${current} / ${total}`;
2475
+ document.getElementById('current-panel-name').textContent = `Panel ${panelName.replace('.pltz.d', '')}`;
2476
+
2477
+ // Update prev/next button states
2478
+ document.getElementById('prev-panel-btn').disabled = currentPanelIndex === 0;
2479
+ document.getElementById('next-panel-btn').disabled = currentPanelIndex === total - 1;
2480
+ }
2481
+
2482
+ // =============================================================================
2483
+ // Canvas Mode (Draggable Panel Layout)
2484
+ // =============================================================================
2485
+ let canvasMode = 'grid'; // 'grid' or 'canvas'
2486
+ let panelPositions = {}; // Store panel positions {name: {x, y, width, height}}
2487
+ let draggedPanel = null;
2488
+ let dragOffset = {x: 0, y: 0};
2489
+
2490
+ function setCanvasMode(mode) {
2491
+ canvasMode = mode;
2492
+ const gridEl = document.getElementById('panel-grid');
2493
+ const canvasEl = document.getElementById('panel-canvas');
2494
+ const gridBtn = document.getElementById('view-grid-btn');
2495
+ const canvasBtn = document.getElementById('view-canvas-btn');
2496
+
2497
+ if (mode === 'grid') {
2498
+ gridEl.style.display = 'grid';
2499
+ canvasEl.style.display = 'none';
2500
+ gridBtn.classList.remove('btn-secondary');
2501
+ gridBtn.classList.add('btn-primary');
2502
+ canvasBtn.classList.add('btn-secondary');
2503
+ canvasBtn.classList.remove('btn-primary');
2504
+ } else {
2505
+ gridEl.style.display = 'none';
2506
+ canvasEl.style.display = 'block';
2507
+ canvasBtn.classList.remove('btn-secondary');
2508
+ canvasBtn.classList.add('btn-primary');
2509
+ gridBtn.classList.add('btn-secondary');
2510
+ gridBtn.classList.remove('btn-primary');
2511
+ renderPanelCanvas();
2512
+ }
2513
+ }
2514
+
2515
+ async function renderPanelCanvas() {
2516
+ const canvasEl = document.getElementById('panel-canvas');
2517
+ if (!panelData || !canvasEl) return;
2518
+
2519
+ // Fetch panels if not cached
2520
+ try {
2521
+ const resp = await fetch('/panels');
2522
+ const data = await resp.json();
2523
+ if (data.error) return;
2524
+
2525
+ canvasEl.innerHTML = '';
2526
+
2527
+ // Calculate canvas size based on number of panels
2528
+ const numPanels = data.panels.length;
2529
+ const cols = Math.ceil(Math.sqrt(numPanels));
2530
+ const baseWidth = 200;
2531
+ const baseHeight = 150;
2532
+ const padding = 20;
2533
+
2534
+ data.panels.forEach((panel, idx) => {
2535
+ const name = panel.name;
2536
+
2537
+ // Initialize position if not set
2538
+ if (!panelPositions[name]) {
2539
+ const col = idx % cols;
2540
+ const row = Math.floor(idx / cols);
2541
+ panelPositions[name] = {
2542
+ x: padding + col * (baseWidth + padding),
2543
+ y: padding + row * (baseHeight + padding),
2544
+ width: baseWidth,
2545
+ height: baseHeight,
2546
+ };
2547
+ }
2548
+
2549
+ const pos = panelPositions[name];
2550
+ const item = document.createElement('div');
2551
+ item.className = 'panel-canvas-item' + (idx === currentPanelIndex ? ' active' : '');
2552
+ item.dataset.panelIndex = idx;
2553
+ item.dataset.panelName = name;
2554
+ item.style.left = pos.x + 'px';
2555
+ item.style.top = pos.y + 'px';
2556
+ item.style.width = pos.width + 'px';
2557
+ item.style.height = pos.height + 'px';
2558
+
2559
+ item.innerHTML = `
2560
+ <span class="panel-canvas-label">Panel ${name}</span>
2561
+ ${panel.image ? `<img src="data:image/png;base64,${panel.image}" alt="Panel ${name}">` : '<div style="padding: 20px; color: var(--text-muted);">No preview</div>'}
2562
+ <div class="panel-canvas-resize"></div>
2563
+ `;
2564
+
2565
+ // Double-click to edit
2566
+ item.addEventListener('dblclick', () => selectPanel(idx));
2567
+
2568
+ // Drag start
2569
+ item.addEventListener('mousedown', (e) => {
2570
+ if (e.target.classList.contains('panel-canvas-resize')) {
2571
+ startResize(e, item, name);
2572
+ } else {
2573
+ startDrag(e, item, name);
2574
+ }
2575
+ });
2576
+
2577
+ canvasEl.appendChild(item);
2578
+ });
2579
+
2580
+ // Update canvas height to fit all panels
2581
+ const maxY = Math.max(...Object.values(panelPositions).map(p => p.y + p.height)) + padding;
2582
+ canvasEl.style.minHeight = Math.max(400, maxY) + 'px';
2583
+
2584
+ } catch (e) {
2585
+ console.error('Error rendering canvas:', e);
2586
+ }
2587
+ }
2588
+
2589
+ function startDrag(e, item, name) {
2590
+ e.preventDefault();
2591
+ draggedPanel = {item, name};
2592
+ dragOffset.x = e.clientX - item.offsetLeft;
2593
+ dragOffset.y = e.clientY - item.offsetTop;
2594
+ item.classList.add('dragging');
2595
+
2596
+ document.addEventListener('mousemove', onDrag);
2597
+ document.addEventListener('mouseup', stopDrag);
2598
+ }
2599
+
2600
+ function onDrag(e) {
2601
+ if (!draggedPanel) return;
2602
+ const canvasEl = document.getElementById('panel-canvas');
2603
+ const rect = canvasEl.getBoundingClientRect();
2604
+
2605
+ let newX = e.clientX - dragOffset.x;
2606
+ let newY = e.clientY - dragOffset.y;
2607
+
2608
+ // Constrain to canvas bounds
2609
+ newX = Math.max(0, Math.min(newX, canvasEl.offsetWidth - draggedPanel.item.offsetWidth));
2610
+ newY = Math.max(0, newY);
2611
+
2612
+ draggedPanel.item.style.left = newX + 'px';
2613
+ draggedPanel.item.style.top = newY + 'px';
2614
+
2615
+ panelPositions[draggedPanel.name].x = newX;
2616
+ panelPositions[draggedPanel.name].y = newY;
2617
+ }
2618
+
2619
+ function stopDrag() {
2620
+ if (draggedPanel) {
2621
+ draggedPanel.item.classList.remove('dragging');
2622
+ draggedPanel = null;
2623
+ }
2624
+ document.removeEventListener('mousemove', onDrag);
2625
+ document.removeEventListener('mouseup', stopDrag);
2626
+ }
2627
+
2628
+ let resizingPanel = null;
2629
+
2630
+ function startResize(e, item, name) {
2631
+ e.preventDefault();
2632
+ e.stopPropagation();
2633
+ resizingPanel = {item, name, startX: e.clientX, startY: e.clientY, startW: item.offsetWidth, startH: item.offsetHeight};
2634
+
2635
+ document.addEventListener('mousemove', onResize);
2636
+ document.addEventListener('mouseup', stopResize);
2637
+ }
2638
+
2639
+ function onResize(e) {
2640
+ if (!resizingPanel) return;
2641
+ const newW = Math.max(100, resizingPanel.startW + (e.clientX - resizingPanel.startX));
2642
+ const newH = Math.max(80, resizingPanel.startH + (e.clientY - resizingPanel.startY));
2643
+
2644
+ resizingPanel.item.style.width = newW + 'px';
2645
+ resizingPanel.item.style.height = newH + 'px';
2646
+
2647
+ panelPositions[resizingPanel.name].width = newW;
2648
+ panelPositions[resizingPanel.name].height = newH;
2649
+ }
2650
+
2651
+ function stopResize() {
2652
+ resizingPanel = null;
2653
+ document.removeEventListener('mousemove', onResize);
2654
+ document.removeEventListener('mouseup', stopResize);
2655
+ }
2656
+
2657
+ // Initialize hover system for a specific element (img or svg)
2658
+ function initHoverSystemForElement(el) {
2659
+ if (!el) return;
2660
+
2661
+ el.addEventListener('mousemove', (e) => {
2662
+ if (imgSize.width === 0 || imgSize.height === 0) return;
2663
+
2664
+ const rect = el.getBoundingClientRect();
2665
+ const x = e.clientX - rect.left;
2666
+ const y = e.clientY - rect.top;
2667
+
2668
+ const scaleX = imgSize.width / rect.width;
2669
+ const scaleY = imgSize.height / rect.height;
2670
+ const imgX = x * scaleX;
2671
+ const imgY = y * scaleY;
2672
+
2673
+ const element = findElementAt(imgX, imgY);
2674
+ if (element !== hoveredElement) {
2675
+ hoveredElement = element;
2676
+ updateOverlay();
2677
+ }
2678
+ });
2679
+
2680
+ el.addEventListener('mouseleave', () => {
2681
+ hoveredElement = null;
2682
+ updateOverlay();
2683
+ });
2684
+
2685
+ el.addEventListener('click', (e) => {
2686
+ if (hoveredElement) {
2687
+ selectedElement = hoveredElement;
2688
+ updateOverlay();
2689
+ scrollToSection(selectedElement);
2690
+ }
2691
+ });
2692
+ }
2693
+
2694
+ // Traces list management
2695
+ function updateTracesList() {
2696
+ const list = document.getElementById('traces-list');
2697
+ if (!traces || traces.length === 0) {
2698
+ list.innerHTML = '<div style="padding: 10px; color: var(--text-muted); font-size: 0.85em;">No traces found in metadata</div>';
2699
+ return;
2700
+ }
2701
+
2702
+ list.innerHTML = traces.map((t, i) => `
2703
+ <div class="trace-item">
2704
+ <input type="color" class="trace-color" value="${t.color || '#1f77b4'}"
2705
+ onchange="updateTraceColor(${i}, this.value)">
2706
+ <span class="trace-label">${t.label || t.id || 'Trace ' + (i+1)}</span>
2707
+ <div class="trace-style">
2708
+ <select onchange="updateTraceStyle(${i}, this.value)">
2709
+ <option value="-" ${t.linestyle === '-' ? 'selected' : ''}>Solid</option>
2710
+ <option value="--" ${t.linestyle === '--' ? 'selected' : ''}>Dashed</option>
2711
+ <option value=":" ${t.linestyle === ':' ? 'selected' : ''}>Dotted</option>
2712
+ <option value="-." ${t.linestyle === '-.' ? 'selected' : ''}>Dash-dot</option>
2713
+ </select>
2714
+ </div>
2715
+ </div>
2716
+ `).join('');
2717
+ }
2718
+
2719
+ function updateTraceColor(idx, color) {
2720
+ if (traces[idx]) {
2721
+ traces[idx].color = color;
2722
+ scheduleUpdate();
2723
+ }
2724
+ }
2725
+
2726
+ function updateTraceStyle(idx, style) {
2727
+ if (traces[idx]) {
2728
+ traces[idx].linestyle = style;
2729
+ scheduleUpdate();
2730
+ }
2731
+ }
2732
+
2733
+ function collectOverrides() {
2734
+ const o = {};
2735
+
2736
+ // Labels - Title
2737
+ const title = document.getElementById('title').value;
2738
+ if (title) o.title = title;
2739
+ o.show_title = document.getElementById('show_title').checked;
2740
+ o.title_fontsize = parseInt(document.getElementById('title_fontsize').value) || 8;
2741
+ // Labels - Caption
2742
+ const caption = document.getElementById('caption').value;
2743
+ if (caption) o.caption = caption;
2744
+ o.show_caption = document.getElementById('show_caption').checked;
2745
+ o.caption_fontsize = parseInt(document.getElementById('caption_fontsize').value) || 7;
2746
+ // Labels - Axis
2747
+ const xlabel = document.getElementById('xlabel').value;
2748
+ const ylabel = document.getElementById('ylabel').value;
2749
+ if (xlabel) o.xlabel = xlabel;
2750
+ if (ylabel) o.ylabel = ylabel;
2751
+
2752
+ // Axis limits
2753
+ const xmin = document.getElementById('xmin').value;
2754
+ const xmax = document.getElementById('xmax').value;
2755
+ if (xmin !== '' && xmax !== '') o.xlim = [parseFloat(xmin), parseFloat(xmax)];
2756
+
2757
+ const ymin = document.getElementById('ymin').value;
2758
+ const ymax = document.getElementById('ymax').value;
2759
+ if (ymin !== '' && ymax !== '') o.ylim = [parseFloat(ymin), parseFloat(ymax)];
2760
+
2761
+ // Traces
2762
+ o.traces = traces;
2763
+
2764
+ // Legend
2765
+ o.legend_visible = document.getElementById('legend_visible').checked;
2766
+ o.legend_loc = document.getElementById('legend_loc').value;
2767
+ o.legend_frameon = document.getElementById('legend_frameon').checked;
2768
+ o.legend_fontsize = parseInt(document.getElementById('legend_fontsize').value) || 6;
2769
+ o.legend_ncols = parseInt(document.getElementById('legend_ncols').value) || 1;
2770
+ o.legend_x = parseFloat(document.getElementById('legend_x').value) || 0.95;
2771
+ o.legend_y = parseFloat(document.getElementById('legend_y').value) || 0.95;
2772
+
2773
+ // Axis and Ticks - X Axis (Bottom)
2774
+ o.x_n_ticks = parseInt(document.getElementById('x_n_ticks').value) || 4;
2775
+ o.hide_x_ticks = document.getElementById('hide_x_ticks').checked;
2776
+ o.x_tick_fontsize = parseInt(document.getElementById('x_tick_fontsize').value) || 7;
2777
+ o.x_tick_direction = document.getElementById('x_tick_direction').value;
2778
+ o.x_tick_length = parseFloat(document.getElementById('x_tick_length').value) || 0.8;
2779
+ o.x_tick_width = parseFloat(document.getElementById('x_tick_width').value) || 0.2;
2780
+ // X Axis (Top)
2781
+ o.show_x_top = document.getElementById('show_x_top').checked;
2782
+ o.x_top_mirror = document.getElementById('x_top_mirror').checked;
2783
+ // Y Axis (Left)
2784
+ o.y_n_ticks = parseInt(document.getElementById('y_n_ticks').value) || 4;
2785
+ o.hide_y_ticks = document.getElementById('hide_y_ticks').checked;
2786
+ o.y_tick_fontsize = parseInt(document.getElementById('y_tick_fontsize').value) || 7;
2787
+ o.y_tick_direction = document.getElementById('y_tick_direction').value;
2788
+ o.y_tick_length = parseFloat(document.getElementById('y_tick_length').value) || 0.8;
2789
+ o.y_tick_width = parseFloat(document.getElementById('y_tick_width').value) || 0.2;
2790
+ // Y Axis (Right)
2791
+ o.show_y_right = document.getElementById('show_y_right').checked;
2792
+ o.y_right_mirror = document.getElementById('y_right_mirror').checked;
2793
+ // Spines
2794
+ o.hide_bottom_spine = document.getElementById('hide_bottom_spine').checked;
2795
+ o.hide_left_spine = document.getElementById('hide_left_spine').checked;
2796
+ // Z Axis (3D)
2797
+ o.hide_z_ticks = document.getElementById('hide_z_ticks').checked;
2798
+ o.z_n_ticks = parseInt(document.getElementById('z_n_ticks').value) || 4;
2799
+ o.z_tick_fontsize = parseInt(document.getElementById('z_tick_fontsize').value) || 7;
2800
+ o.z_tick_direction = document.getElementById('z_tick_direction').value;
2801
+
2802
+ // Style
2803
+ o.grid = document.getElementById('grid').checked;
2804
+ o.hide_top_spine = document.getElementById('hide_top_spine').checked;
2805
+ o.hide_right_spine = document.getElementById('hide_right_spine').checked;
2806
+ o.axis_width = parseFloat(document.getElementById('axis_width').value) || 0.2;
2807
+ o.axis_fontsize = parseInt(document.getElementById('axis_fontsize').value) || 7;
2808
+ o.facecolor = document.getElementById('facecolor').value;
2809
+ o.transparent = document.getElementById('transparent').value === 'true';
2810
+
2811
+ // Dimensions (always in inches for matplotlib)
2812
+ o.fig_size = getFigSizeInches();
2813
+ o.dpi = parseInt(document.getElementById('dpi').value) || 300;
2814
+
2815
+ // Annotations
2816
+ o.annotations = overrides.annotations || [];
2817
+
2818
+ // Element-specific overrides (per-element styles)
2819
+ if (overrides.element_overrides) {
2820
+ o.element_overrides = overrides.element_overrides;
2821
+ }
2822
+
2823
+ return o;
2824
+ }
2825
+
2826
+ async function updatePreview(forceUpdate = false) {
2827
+ // Skip auto-update if showing original preview (user hasn't explicitly requested update)
2828
+ if (isShowingOriginalPreview && !forceUpdate) {
2829
+ console.log('Skipping auto-update: showing original preview');
2830
+ return;
2831
+ }
2832
+
2833
+ setStatus('Updating...', false);
2834
+ overrides = collectOverrides();
2835
+
2836
+ // Preserve current selection to restore after update
2837
+ const previousSelection = selectedElement;
2838
+
2839
+ try {
2840
+ const darkMode = isDarkMode();
2841
+ const resp = await fetch('/update', {
2842
+ method: 'POST',
2843
+ headers: {'Content-Type': 'application/json'},
2844
+ body: JSON.stringify({overrides, dark_mode: darkMode})
2845
+ });
2846
+ const data = await resp.json();
2847
+
2848
+ // Remove SVG wrapper if exists, show img element for re-rendered preview
2849
+ const existingSvgWrapper = document.getElementById('preview-svg-wrapper');
2850
+ if (existingSvgWrapper) {
2851
+ existingSvgWrapper.remove();
2852
+ }
2853
+ const imgEl = document.getElementById('preview-img');
2854
+ imgEl.style.display = 'block';
2855
+ imgEl.src = 'data:image/png;base64,' + data.image;
2856
+
2857
+ // Mark that we're no longer showing original preview
2858
+ isShowingOriginalPreview = false;
2859
+
2860
+ if (data.bboxes) {
2861
+ elementBboxes = data.bboxes;
2862
+ // Store schema v0.3 metadata if available
2863
+ if (data.bboxes._meta) {
2864
+ schemaMeta = data.bboxes._meta;
2865
+ console.log('Schema v0.3 geometry available:', schemaMeta.schema_version);
2866
+ }
2867
+ }
2868
+ if (data.img_size) {
2869
+ imgSize = data.img_size;
2870
+ }
2871
+
2872
+ // Restore selection if the element still exists in the new bboxes
2873
+ if (previousSelection && elementBboxes[previousSelection]) {
2874
+ selectedElement = previousSelection;
2875
+ } else {
2876
+ selectedElement = null;
2877
+ }
2878
+ hoveredElement = null;
2879
+ updateOverlay();
2880
+
2881
+ setStatus('Preview updated', false);
2882
+ } catch (e) {
2883
+ setStatus('Error: ' + e.message, true);
2884
+ }
2885
+ }
2886
+
2887
+ // Restore original preview (SVG/PNG from bundle)
2888
+ async function restoreOriginalPreview() {
2889
+ if (!originalBboxes || !originalImgSize) {
2890
+ console.log('No original preview to restore');
2891
+ return;
2892
+ }
2893
+
2894
+ setStatus('Restoring original preview...', false);
2895
+ await loadInitialPreview();
2896
+ }
2897
+
2898
+ async function saveManual() {
2899
+ setStatus('Saving...', false);
2900
+ try {
2901
+ const resp = await fetch('/save', {
2902
+ method: 'POST',
2903
+ headers: {'Content-Type': 'application/json'}
2904
+ });
2905
+ const data = await resp.json();
2906
+ if (data.status === 'saved') {
2907
+ setStatus('Saved: ' + data.path.split('/').pop(), false);
2908
+ } else {
2909
+ setStatus('Error: ' + data.message, true);
2910
+ }
2911
+ } catch (e) {
2912
+ setStatus('Error: ' + e.message, true);
2913
+ }
2914
+ }
2915
+
2916
+ function resetOverrides() {
2917
+ if (confirm('Reset all changes to original values?')) {
2918
+ location.reload();
2919
+ }
2920
+ }
2921
+
2922
+ function addAnnotation() {
2923
+ const text = document.getElementById('annot-text').value;
2924
+ if (!text) return;
2925
+ const x = parseFloat(document.getElementById('annot-x').value) || 0.5;
2926
+ const y = parseFloat(document.getElementById('annot-y').value) || 0.5;
2927
+ const size = parseInt(document.getElementById('annot-size').value) || 8;
2928
+ if (!overrides.annotations) overrides.annotations = [];
2929
+ overrides.annotations.push({type: 'text', text, x, y, fontsize: size});
2930
+ document.getElementById('annot-text').value = '';
2931
+ updateAnnotationsList();
2932
+ updatePreview();
2933
+ }
2934
+
2935
+ function removeAnnotation(idx) {
2936
+ overrides.annotations.splice(idx, 1);
2937
+ updateAnnotationsList();
2938
+ updatePreview();
2939
+ }
2940
+
2941
+ function updateAnnotationsList() {
2942
+ const list = document.getElementById('annotations-list');
2943
+ const annotations = overrides.annotations || [];
2944
+ if (annotations.length === 0) {
2945
+ list.innerHTML = '';
2946
+ return;
2947
+ }
2948
+ list.innerHTML = annotations.map((a, i) =>
2949
+ `<div class="annotation-item">
2950
+ <span>${a.text.substring(0, 25)}${a.text.length > 25 ? '...' : ''} (${a.x.toFixed(2)}, ${a.y.toFixed(2)})</span>
2951
+ <button onclick="removeAnnotation(${i})">Remove</button>
2952
+ </div>`
2953
+ ).join('');
2954
+ }
2955
+
2956
+ // =============================================================================
2957
+ // Statistics Display
2958
+ // =============================================================================
2959
+ async function refreshStats() {
2960
+ const container = document.getElementById('stats-container');
2961
+ container.innerHTML = '<div class="stats-loading">Loading statistics...</div>';
2962
+
2963
+ try {
2964
+ const resp = await fetch('/stats');
2965
+ const data = await resp.json();
2966
+
2967
+ if (!data.has_stats) {
2968
+ container.innerHTML = '<div class="stats-empty">No statistical tests in this figure</div>';
2969
+ return;
2970
+ }
2971
+
2972
+ let html = '';
2973
+
2974
+ // Show summary if available
2975
+ if (data.stats_summary) {
2976
+ const summary = data.stats_summary;
2977
+ html += `
2978
+ <div class="stats-summary-header">
2979
+ ${summary.test_type.replace('_', '-')}
2980
+ <span class="stats-correction-badge">${summary.correction_method}</span>
2981
+ </div>
2982
+ <div class="stats-summary-body">
2983
+ <div class="stats-row">
2984
+ <span class="stats-label">Comparisons:</span>
2985
+ <span class="stats-value">${summary.n_comparisons}</span>
2986
+ </div>
2987
+ <div class="stats-row">
2988
+ <span class="stats-label">α (original):</span>
2989
+ <span class="stats-value">${summary.alpha}</span>
2990
+ </div>
2991
+ <div class="stats-row">
2992
+ <span class="stats-label">α (corrected):</span>
2993
+ <span class="stats-value">${summary.corrected_alpha.toFixed(4)}</span>
2994
+ </div>
2995
+ </div>
2996
+ `;
2997
+ }
2998
+
2999
+ // Show individual test results
3000
+ data.stats.forEach((stat, idx) => {
3001
+ const sigClass = getSigClass(stat.stars);
3002
+ const samples = stat.samples || {};
3003
+ const correction = stat.correction || {};
3004
+
3005
+ html += `
3006
+ <div class="stats-card">
3007
+ <div class="stats-card-header">
3008
+ <span class="stats-card-title">
3009
+ ${samples.group1?.name || 'Group 1'} vs ${samples.group2?.name || 'Group 2'}
3010
+ </span>
3011
+ <span class="stats-significance ${sigClass}">${stat.stars}</span>
3012
+ </div>
3013
+ <div class="stats-row">
3014
+ <span class="stats-label">${stat.statistic?.name || 'Stat'}:</span>
3015
+ <span class="stats-value">${(stat.statistic?.value || 0).toFixed(3)}</span>
3016
+ </div>
3017
+ <div class="stats-row">
3018
+ <span class="stats-label">p (raw):</span>
3019
+ <span class="stats-value">${stat.p_value.toFixed(4)}</span>
3020
+ </div>
3021
+ ${correction.corrected_p ? `
3022
+ <div class="stats-row">
3023
+ <span class="stats-label">p (corrected):</span>
3024
+ <span class="stats-value">${correction.corrected_p.toFixed(4)}</span>
3025
+ </div>` : ''}
3026
+ <div class="stats-groups">
3027
+ ${samples.group1 ? renderGroupStats(samples.group1) : ''}
3028
+ ${samples.group2 ? renderGroupStats(samples.group2) : ''}
3029
+ </div>
3030
+ </div>
3031
+ `;
3032
+ });
3033
+
3034
+ container.innerHTML = html;
3035
+ } catch (e) {
3036
+ container.innerHTML = `<div class="stats-empty">Error loading stats: ${e.message}</div>`;
3037
+ }
3038
+ }
3039
+
3040
+ function getSigClass(stars) {
3041
+ if (stars === '***') return 'sig-high';
3042
+ if (stars === '**') return 'sig-medium';
3043
+ if (stars === '*') return 'sig-low';
3044
+ return 'sig-ns';
3045
+ }
3046
+
3047
+ function renderGroupStats(group) {
3048
+ return `
3049
+ <div class="stats-group">
3050
+ <div class="stats-group-name">${group.name || 'Group'}</div>
3051
+ <div>n = ${group.n}</div>
3052
+ <div>μ = ${group.mean?.toFixed(2) || '-'}</div>
3053
+ <div>σ = ${group.std?.toFixed(2) || '-'}</div>
3054
+ </div>
3055
+ `;
3056
+ }
3057
+
3058
+ function setStatus(msg, isError = false) {
3059
+ const el = document.getElementById('status');
3060
+ const loadingOverlay = document.getElementById('loading-overlay');
3061
+
3062
+ // Show/hide spinner for loading states
3063
+ if (msg === 'Updating...' || msg === 'Loading preview...') {
3064
+ loadingOverlay.style.display = 'flex';
3065
+ el.textContent = ''; // Clear status text during loading
3066
+ } else {
3067
+ loadingOverlay.style.display = 'none';
3068
+ el.textContent = msg;
3069
+ }
3070
+ el.classList.toggle('error', isError);
3071
+ }
3072
+
3073
+ // Debounced auto-update
3074
+ let updateTimer = null;
3075
+ const DEBOUNCE_DELAY = 500;
3076
+
3077
+ function scheduleUpdate() {
3078
+ if (updateTimer) clearTimeout(updateTimer);
3079
+ updateTimer = setTimeout(() => {
3080
+ updatePreview();
3081
+ }, DEBOUNCE_DELAY);
3082
+ }
3083
+
3084
+ // Auto-update on input changes
3085
+ document.querySelectorAll('input[type="text"], input[type="number"], textarea').forEach(el => {
3086
+ el.addEventListener('input', scheduleUpdate);
3087
+ el.addEventListener('keypress', (e) => {
3088
+ if (e.key === 'Enter' && el.tagName !== 'TEXTAREA') {
3089
+ if (updateTimer) clearTimeout(updateTimer);
3090
+ updatePreview();
3091
+ }
3092
+ });
3093
+ });
3094
+
3095
+ document.querySelectorAll('input[type="checkbox"], select').forEach(el => {
3096
+ el.addEventListener('change', () => {
3097
+ if (updateTimer) clearTimeout(updateTimer);
3098
+ updatePreview();
3099
+ });
3100
+ });
3101
+
3102
+ document.querySelectorAll('input[type="color"]').forEach(el => {
3103
+ el.addEventListener('change', () => {
3104
+ if (updateTimer) clearTimeout(updateTimer);
3105
+ updatePreview();
3106
+ });
3107
+ });
3108
+
3109
+ // Ctrl+S keyboard shortcut to save
3110
+ document.addEventListener('keydown', (e) => {
3111
+ if ((e.ctrlKey || e.metaKey) && e.key === 's') {
3112
+ e.preventDefault();
3113
+ saveManual();
3114
+ }
3115
+ });
3116
+
3117
+ // Auto-update interval system
3118
+ let autoUpdateIntervalId = null;
3119
+
3120
+ function setAutoUpdateInterval() {
3121
+ if (autoUpdateIntervalId) {
3122
+ clearInterval(autoUpdateIntervalId);
3123
+ autoUpdateIntervalId = null;
3124
+ }
3125
+
3126
+ const intervalMs = parseInt(document.getElementById('auto_update_interval').value);
3127
+ if (intervalMs > 0) {
3128
+ autoUpdateIntervalId = setInterval(() => {
3129
+ updatePreview();
3130
+ }, intervalMs);
3131
+ }
3132
+ }
3133
+ """
3134
+
3135
+
3136
+ # EOF