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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (1092) hide show
  1. scitex/__init__.py +19 -8
  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/benchmark/__init__.py +15 -25
  66. scitex/benchmark/benchmark.py +124 -117
  67. scitex/benchmark/monitor.py +117 -107
  68. scitex/benchmark/profiler.py +61 -58
  69. scitex/bridge/__init__.py +110 -0
  70. scitex/bridge/_helpers.py +149 -0
  71. scitex/bridge/_plt_vis.py +529 -0
  72. scitex/bridge/_protocol.py +283 -0
  73. scitex/bridge/_stats_plt.py +261 -0
  74. scitex/bridge/_stats_vis.py +265 -0
  75. scitex/browser/__init__.py +0 -2
  76. scitex/browser/auth/__init__.py +0 -0
  77. scitex/browser/auth/google.py +16 -11
  78. scitex/browser/automation/CookieHandler.py +2 -3
  79. scitex/browser/collaboration/__init__.py +3 -0
  80. scitex/browser/collaboration/auth_helpers.py +3 -1
  81. scitex/browser/collaboration/collaborative_agent.py +2 -0
  82. scitex/browser/collaboration/interactive_panel.py +2 -2
  83. scitex/browser/collaboration/shared_session.py +20 -11
  84. scitex/browser/collaboration/standard_interactions.py +1 -0
  85. scitex/browser/core/BrowserMixin.py +12 -30
  86. scitex/browser/core/ChromeProfileManager.py +9 -24
  87. scitex/browser/debugging/_browser_logger.py +15 -25
  88. scitex/browser/debugging/_failure_capture.py +9 -2
  89. scitex/browser/debugging/_highlight_element.py +15 -6
  90. scitex/browser/debugging/_show_grid.py +5 -6
  91. scitex/browser/debugging/_sync_session.py +4 -3
  92. scitex/browser/debugging/_test_monitor.py +14 -5
  93. scitex/browser/debugging/_visual_cursor.py +46 -35
  94. scitex/browser/interaction/click_center.py +4 -3
  95. scitex/browser/interaction/click_with_fallbacks.py +7 -10
  96. scitex/browser/interaction/close_popups.py +79 -66
  97. scitex/browser/interaction/fill_with_fallbacks.py +8 -8
  98. scitex/browser/pdf/__init__.py +3 -1
  99. scitex/browser/pdf/click_download_for_chrome_pdf_viewer.py +11 -10
  100. scitex/browser/pdf/detect_chrome_pdf_viewer.py +3 -6
  101. scitex/browser/remote/CaptchaHandler.py +109 -96
  102. scitex/browser/remote/ZenRowsAPIClient.py +91 -97
  103. scitex/browser/remote/ZenRowsBrowserManager.py +138 -112
  104. scitex/browser/stealth/HumanBehavior.py +4 -9
  105. scitex/browser/stealth/StealthManager.py +11 -26
  106. scitex/capture/__init__.py +17 -17
  107. scitex/capture/__main__.py +2 -3
  108. scitex/capture/capture.py +23 -51
  109. scitex/capture/cli.py +14 -39
  110. scitex/capture/gif.py +5 -9
  111. scitex/capture/mcp_server.py +7 -20
  112. scitex/capture/session.py +4 -3
  113. scitex/capture/utils.py +18 -53
  114. scitex/cli/__init__.py +1 -1
  115. scitex/cli/cloud.py +158 -116
  116. scitex/cli/config.py +224 -0
  117. scitex/cli/main.py +41 -40
  118. scitex/cli/scholar.py +60 -27
  119. scitex/cli/security.py +14 -20
  120. scitex/cli/web.py +87 -90
  121. scitex/cli/writer.py +51 -45
  122. scitex/cloud/__init__.py +14 -11
  123. scitex/cloud/_matplotlib_hook.py +6 -6
  124. scitex/config/README.md +313 -0
  125. scitex/config/{PriorityConfig.py → _PriorityConfig.py} +114 -17
  126. scitex/config/_ScitexConfig.py +319 -0
  127. scitex/config/__init__.py +41 -9
  128. scitex/config/_paths.py +325 -0
  129. scitex/config/default.yaml +81 -0
  130. scitex/context/_suppress_output.py +2 -3
  131. scitex/db/_BaseMixins/_BaseBackupMixin.py +3 -1
  132. scitex/db/_BaseMixins/_BaseBatchMixin.py +3 -1
  133. scitex/db/_BaseMixins/_BaseBlobMixin.py +3 -1
  134. scitex/db/_BaseMixins/_BaseImportExportMixin.py +1 -3
  135. scitex/db/_BaseMixins/_BaseIndexMixin.py +3 -1
  136. scitex/db/_BaseMixins/_BaseMaintenanceMixin.py +1 -3
  137. scitex/db/_BaseMixins/_BaseQueryMixin.py +3 -1
  138. scitex/db/_BaseMixins/_BaseRowMixin.py +3 -1
  139. scitex/db/_BaseMixins/_BaseTableMixin.py +3 -1
  140. scitex/db/_BaseMixins/_BaseTransactionMixin.py +1 -3
  141. scitex/db/_BaseMixins/__init__.py +1 -1
  142. scitex/db/__init__.py +9 -1
  143. scitex/db/__main__.py +8 -21
  144. scitex/db/_check_health.py +15 -31
  145. scitex/db/_delete_duplicates.py +7 -4
  146. scitex/db/_inspect.py +22 -38
  147. scitex/db/_inspect_optimized.py +89 -85
  148. scitex/db/_postgresql/_PostgreSQL.py +0 -1
  149. scitex/db/_postgresql/_PostgreSQLMixins/_BlobMixin.py +3 -1
  150. scitex/db/_postgresql/_PostgreSQLMixins/_ConnectionMixin.py +1 -3
  151. scitex/db/_postgresql/_PostgreSQLMixins/_ImportExportMixin.py +1 -3
  152. scitex/db/_postgresql/_PostgreSQLMixins/_MaintenanceMixin.py +1 -4
  153. scitex/db/_postgresql/_PostgreSQLMixins/_QueryMixin.py +3 -3
  154. scitex/db/_postgresql/_PostgreSQLMixins/_RowMixin.py +3 -1
  155. scitex/db/_postgresql/_PostgreSQLMixins/_TransactionMixin.py +1 -3
  156. scitex/db/_postgresql/__init__.py +1 -1
  157. scitex/db/_sqlite3/_SQLite3.py +2 -4
  158. scitex/db/_sqlite3/_SQLite3Mixins/_ArrayMixin.py +11 -12
  159. scitex/db/_sqlite3/_SQLite3Mixins/_ArrayMixin_v01-need-_hash-col.py +19 -14
  160. scitex/db/_sqlite3/_SQLite3Mixins/_BatchMixin.py +3 -1
  161. scitex/db/_sqlite3/_SQLite3Mixins/_BlobMixin.py +7 -7
  162. scitex/db/_sqlite3/_SQLite3Mixins/_ColumnMixin.py +118 -111
  163. scitex/db/_sqlite3/_SQLite3Mixins/_ConnectionMixin.py +8 -10
  164. scitex/db/_sqlite3/_SQLite3Mixins/_GitMixin.py +17 -45
  165. scitex/db/_sqlite3/_SQLite3Mixins/_ImportExportMixin.py +1 -3
  166. scitex/db/_sqlite3/_SQLite3Mixins/_IndexMixin.py +3 -1
  167. scitex/db/_sqlite3/_SQLite3Mixins/_QueryMixin.py +3 -4
  168. scitex/db/_sqlite3/_SQLite3Mixins/_RowMixin.py +9 -9
  169. scitex/db/_sqlite3/_SQLite3Mixins/_TableMixin.py +18 -11
  170. scitex/db/_sqlite3/_SQLite3Mixins/__init__.py +1 -0
  171. scitex/db/_sqlite3/__init__.py +1 -1
  172. scitex/db/_sqlite3/_delete_duplicates.py +13 -11
  173. scitex/decorators/__init__.py +29 -4
  174. scitex/decorators/_auto_order.py +43 -43
  175. scitex/decorators/_batch_fn.py +12 -6
  176. scitex/decorators/_cache_disk.py +8 -9
  177. scitex/decorators/_cache_disk_async.py +8 -7
  178. scitex/decorators/_combined.py +19 -13
  179. scitex/decorators/_converters.py +16 -3
  180. scitex/decorators/_deprecated.py +32 -22
  181. scitex/decorators/_numpy_fn.py +18 -4
  182. scitex/decorators/_pandas_fn.py +17 -5
  183. scitex/decorators/_signal_fn.py +17 -3
  184. scitex/decorators/_torch_fn.py +32 -15
  185. scitex/decorators/_xarray_fn.py +23 -9
  186. scitex/dev/_analyze_code_flow.py +0 -2
  187. scitex/dict/_DotDict.py +15 -19
  188. scitex/dict/_flatten.py +1 -0
  189. scitex/dict/_listed_dict.py +1 -0
  190. scitex/dict/_pop_keys.py +1 -0
  191. scitex/dict/_replace.py +1 -0
  192. scitex/dict/_safe_merge.py +1 -0
  193. scitex/dict/_to_str.py +2 -3
  194. scitex/dsp/__init__.py +13 -4
  195. scitex/dsp/_crop.py +3 -1
  196. scitex/dsp/_detect_ripples.py +3 -1
  197. scitex/dsp/_modulation_index.py +3 -1
  198. scitex/dsp/_time.py +3 -1
  199. scitex/dsp/_wavelet.py +0 -1
  200. scitex/dsp/example.py +0 -5
  201. scitex/dsp/filt.py +4 -0
  202. scitex/dsp/utils/__init__.py +4 -1
  203. scitex/dsp/utils/pac.py +3 -3
  204. scitex/dt/_normalize_timestamp.py +4 -1
  205. scitex/errors.py +3 -6
  206. scitex/etc/__init__.py +1 -1
  207. scitex/gen/_DimHandler.py +6 -6
  208. scitex/gen/__init__.py +5 -1
  209. scitex/gen/_deprecated_close.py +1 -0
  210. scitex/gen/_deprecated_start.py +5 -3
  211. scitex/gen/_detect_environment.py +44 -41
  212. scitex/gen/_detect_notebook_path.py +51 -47
  213. scitex/gen/_embed.py +1 -1
  214. scitex/gen/_get_notebook_path.py +81 -62
  215. scitex/gen/_inspect_module.py +0 -1
  216. scitex/gen/_norm.py +16 -7
  217. scitex/gen/_norm_cache.py +78 -65
  218. scitex/gen/_print_config.py +0 -3
  219. scitex/gen/_src.py +2 -3
  220. scitex/gen/_title_case.py +3 -2
  221. scitex/gen/_to_even.py +8 -8
  222. scitex/gen/_transpose.py +3 -3
  223. scitex/gen/misc.py +0 -3
  224. scitex/gists/_SigMacro_processFigure_S.py +2 -2
  225. scitex/gists/_SigMacro_toBlue.py +2 -2
  226. scitex/gists/__init__.py +4 -1
  227. scitex/git/_branch.py +19 -11
  228. scitex/git/_clone.py +23 -15
  229. scitex/git/_commit.py +10 -12
  230. scitex/git/_init.py +15 -38
  231. scitex/git/_remote.py +9 -3
  232. scitex/git/_result.py +3 -0
  233. scitex/git/_retry.py +2 -5
  234. scitex/git/_types.py +4 -0
  235. scitex/git/_validation.py +8 -8
  236. scitex/git/_workflow.py +4 -4
  237. scitex/io/__init__.py +2 -1
  238. scitex/io/_glob.py +2 -2
  239. scitex/io/_json2md.py +3 -3
  240. scitex/io/_load.py +11 -8
  241. scitex/io/_load_cache.py +71 -71
  242. scitex/io/_load_configs.py +2 -3
  243. scitex/io/_load_modules/_H5Explorer.py +6 -12
  244. scitex/io/_load_modules/_ZarrExplorer.py +3 -3
  245. scitex/io/_load_modules/_bibtex.py +62 -63
  246. scitex/io/_load_modules/_canvas.py +166 -0
  247. scitex/io/_load_modules/_catboost.py +7 -2
  248. scitex/io/_load_modules/_hdf5.py +2 -0
  249. scitex/io/_load_modules/_image.py +5 -1
  250. scitex/io/_load_modules/_matlab.py +3 -1
  251. scitex/io/_load_modules/_optuna.py +0 -1
  252. scitex/io/_load_modules/_pdf.py +38 -29
  253. scitex/io/_load_modules/_sqlite3.py +1 -0
  254. scitex/io/_load_modules/_txt.py +2 -0
  255. scitex/io/_load_modules/_xml.py +9 -9
  256. scitex/io/_load_modules/_zarr.py +12 -10
  257. scitex/io/_metadata.py +76 -37
  258. scitex/io/_qr_utils.py +18 -13
  259. scitex/io/_save.py +228 -63
  260. scitex/io/_save_modules/__init__.py +7 -2
  261. scitex/io/_save_modules/_bibtex.py +66 -61
  262. scitex/io/_save_modules/_canvas.py +355 -0
  263. scitex/io/_save_modules/_catboost.py +2 -2
  264. scitex/io/_save_modules/_csv.py +4 -4
  265. scitex/io/_save_modules/_excel.py +5 -9
  266. scitex/io/_save_modules/_hdf5.py +9 -21
  267. scitex/io/_save_modules/_html.py +5 -5
  268. scitex/io/_save_modules/_image.py +105 -8
  269. scitex/io/_save_modules/_joblib.py +2 -2
  270. scitex/io/_save_modules/_json.py +51 -6
  271. scitex/io/_save_modules/_listed_dfs_as_csv.py +2 -1
  272. scitex/io/_save_modules/_listed_scalars_as_csv.py +2 -1
  273. scitex/io/_save_modules/_matlab.py +2 -2
  274. scitex/io/_save_modules/_numpy.py +6 -8
  275. scitex/io/_save_modules/_pickle.py +4 -4
  276. scitex/io/_save_modules/_plotly.py +3 -3
  277. scitex/io/_save_modules/_tex.py +23 -25
  278. scitex/io/_save_modules/_text.py +2 -2
  279. scitex/io/_save_modules/_yaml.py +9 -9
  280. scitex/io/_save_modules/_zarr.py +15 -15
  281. scitex/io/utils/__init__.py +2 -1
  282. scitex/io/utils/h5_to_zarr.py +173 -155
  283. scitex/linalg/__init__.py +1 -1
  284. scitex/linalg/_geometric_median.py +4 -3
  285. scitex/logging/_Tee.py +5 -7
  286. scitex/logging/__init__.py +18 -19
  287. scitex/logging/_config.py +4 -1
  288. scitex/logging/_context.py +6 -5
  289. scitex/logging/_formatters.py +2 -3
  290. scitex/logging/_handlers.py +19 -20
  291. scitex/logging/_levels.py +9 -17
  292. scitex/logging/_logger.py +74 -15
  293. scitex/logging/_print_capture.py +17 -17
  294. scitex/nn/_BNet.py +1 -3
  295. scitex/nn/_Filters.py +6 -2
  296. scitex/nn/_ModulationIndex.py +3 -1
  297. scitex/nn/_PAC.py +3 -2
  298. scitex/nn/_PSD.py +0 -1
  299. scitex/nn/__init__.py +16 -3
  300. scitex/path/_clean.py +10 -8
  301. scitex/path/_find.py +1 -1
  302. scitex/path/_get_spath.py +1 -2
  303. scitex/path/_mk_spath.py +1 -1
  304. scitex/path/_symlink.py +5 -10
  305. scitex/pd/__init__.py +4 -1
  306. scitex/pd/_force_df.py +24 -24
  307. scitex/pd/_get_unique.py +1 -0
  308. scitex/pd/_merge_columns.py +1 -1
  309. scitex/pd/_round.py +11 -7
  310. scitex/pd/_to_xy.py +0 -1
  311. scitex/plt/REQUESTS.md +191 -0
  312. scitex/plt/__init__.py +185 -87
  313. scitex/plt/_subplots/_AxesWrapper.py +22 -6
  314. scitex/plt/_subplots/_AxisWrapper.py +100 -39
  315. scitex/plt/_subplots/_AxisWrapperMixins/_AdjustmentMixin.py +74 -52
  316. scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin.py +183 -73
  317. scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin.py +61 -45
  318. scitex/plt/_subplots/_AxisWrapperMixins/_TrackingMixin.py +26 -14
  319. scitex/plt/_subplots/_AxisWrapperMixins/_UnitAwareMixin.py +80 -73
  320. scitex/plt/_subplots/_FigWrapper.py +93 -60
  321. scitex/plt/_subplots/_SubplotsWrapper.py +135 -68
  322. scitex/plt/_subplots/__init__.py +10 -0
  323. scitex/plt/_subplots/_export_as_csv.py +89 -47
  324. scitex/plt/_subplots/_export_as_csv_formatters/__init__.py +1 -0
  325. scitex/plt/_subplots/_export_as_csv_formatters/_format_annotate.py +6 -4
  326. scitex/plt/_subplots/_export_as_csv_formatters/_format_bar.py +88 -38
  327. scitex/plt/_subplots/_export_as_csv_formatters/_format_barh.py +25 -31
  328. scitex/plt/_subplots/_export_as_csv_formatters/_format_boxplot.py +53 -23
  329. scitex/plt/_subplots/_export_as_csv_formatters/_format_contour.py +38 -25
  330. scitex/plt/_subplots/_export_as_csv_formatters/_format_contourf.py +17 -9
  331. scitex/plt/_subplots/_export_as_csv_formatters/_format_errorbar.py +70 -124
  332. scitex/plt/_subplots/_export_as_csv_formatters/_format_eventplot.py +12 -10
  333. scitex/plt/_subplots/_export_as_csv_formatters/_format_fill.py +31 -17
  334. scitex/plt/_subplots/_export_as_csv_formatters/_format_fill_between.py +33 -21
  335. scitex/plt/_subplots/_export_as_csv_formatters/_format_hexbin.py +14 -4
  336. scitex/plt/_subplots/_export_as_csv_formatters/_format_hist.py +43 -29
  337. scitex/plt/_subplots/_export_as_csv_formatters/_format_hist2d.py +14 -4
  338. scitex/plt/_subplots/_export_as_csv_formatters/_format_imshow.py +27 -11
  339. scitex/plt/_subplots/_export_as_csv_formatters/_format_imshow2d.py +7 -5
  340. scitex/plt/_subplots/_export_as_csv_formatters/_format_matshow.py +9 -7
  341. scitex/plt/_subplots/_export_as_csv_formatters/_format_pie.py +15 -6
  342. scitex/plt/_subplots/_export_as_csv_formatters/_format_plot.py +128 -34
  343. scitex/plt/_subplots/_export_as_csv_formatters/_format_plot_box.py +52 -27
  344. scitex/plt/_subplots/_export_as_csv_formatters/_format_plot_imshow.py +1 -0
  345. scitex/plt/_subplots/_export_as_csv_formatters/_format_plot_kde.py +16 -17
  346. scitex/plt/_subplots/_export_as_csv_formatters/_format_plot_scatter.py +7 -5
  347. scitex/plt/_subplots/_export_as_csv_formatters/_format_quiver.py +10 -8
  348. scitex/plt/_subplots/_export_as_csv_formatters/_format_scatter.py +17 -6
  349. scitex/plt/_subplots/_export_as_csv_formatters/_format_sns_barplot.py +43 -26
  350. scitex/plt/_subplots/_export_as_csv_formatters/_format_sns_boxplot.py +68 -47
  351. scitex/plt/_subplots/_export_as_csv_formatters/_format_sns_heatmap.py +52 -64
  352. scitex/plt/_subplots/_export_as_csv_formatters/_format_sns_histplot.py +55 -50
  353. scitex/plt/_subplots/_export_as_csv_formatters/_format_sns_jointplot.py +9 -11
  354. scitex/plt/_subplots/_export_as_csv_formatters/_format_sns_kdeplot.py +63 -29
  355. scitex/plt/_subplots/_export_as_csv_formatters/_format_sns_lineplot.py +4 -4
  356. scitex/plt/_subplots/_export_as_csv_formatters/_format_sns_pairplot.py +6 -4
  357. scitex/plt/_subplots/_export_as_csv_formatters/_format_sns_scatterplot.py +44 -40
  358. scitex/plt/_subplots/_export_as_csv_formatters/_format_sns_stripplot.py +46 -39
  359. scitex/plt/_subplots/_export_as_csv_formatters/_format_sns_swarmplot.py +46 -39
  360. scitex/plt/_subplots/_export_as_csv_formatters/_format_sns_violinplot.py +75 -94
  361. scitex/plt/_subplots/_export_as_csv_formatters/_format_stem.py +12 -3
  362. scitex/plt/_subplots/_export_as_csv_formatters/_format_step.py +12 -3
  363. scitex/plt/_subplots/_export_as_csv_formatters/_format_streamplot.py +10 -8
  364. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_conf_mat.py +17 -15
  365. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_ecdf.py +10 -9
  366. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_fillv.py +35 -31
  367. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_heatmap.py +18 -18
  368. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_image.py +24 -18
  369. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_joyplot.py +9 -7
  370. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_line.py +34 -23
  371. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_mean_ci.py +15 -13
  372. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_mean_std.py +12 -10
  373. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_median_iqr.py +15 -13
  374. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_raster.py +11 -9
  375. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_rectangle.py +84 -56
  376. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_scatter_hist.py +35 -32
  377. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_shaded_line.py +46 -30
  378. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_violin.py +51 -51
  379. scitex/plt/_subplots/_export_as_csv_formatters/_format_text.py +32 -31
  380. scitex/plt/_subplots/_export_as_csv_formatters/_format_violin.py +34 -31
  381. scitex/plt/_subplots/_export_as_csv_formatters/_format_violinplot.py +44 -37
  382. scitex/plt/_subplots/_export_as_csv_formatters/verify_formatters.py +91 -74
  383. scitex/plt/_tpl.py +6 -5
  384. scitex/plt/ax/_plot/__init__.py +24 -0
  385. scitex/plt/ax/_plot/_add_fitted_line.py +12 -11
  386. scitex/plt/ax/_plot/_plot_circular_hist.py +3 -1
  387. scitex/plt/ax/_plot/_plot_statistical_shaded_line.py +25 -19
  388. scitex/plt/ax/_plot/_stx_conf_mat.py +6 -3
  389. scitex/plt/ax/_plot/_stx_ecdf.py +5 -3
  390. scitex/plt/ax/_plot/_stx_fillv.py +4 -2
  391. scitex/plt/ax/_plot/_stx_heatmap.py +7 -4
  392. scitex/plt/ax/_plot/_stx_image.py +7 -5
  393. scitex/plt/ax/_plot/_stx_joyplot.py +32 -10
  394. scitex/plt/ax/_plot/_stx_raster.py +26 -11
  395. scitex/plt/ax/_plot/_stx_rectangle.py +2 -2
  396. scitex/plt/ax/_plot/_stx_shaded_line.py +15 -11
  397. scitex/plt/ax/_plot/_stx_violin.py +3 -1
  398. scitex/plt/ax/_style/_add_marginal_ax.py +6 -4
  399. scitex/plt/ax/_style/_auto_scale_axis.py +14 -10
  400. scitex/plt/ax/_style/_extend.py +3 -1
  401. scitex/plt/ax/_style/_force_aspect.py +5 -3
  402. scitex/plt/ax/_style/_format_units.py +2 -2
  403. scitex/plt/ax/_style/_hide_spines.py +5 -1
  404. scitex/plt/ax/_style/_map_ticks.py +5 -3
  405. scitex/plt/ax/_style/_rotate_labels.py +5 -4
  406. scitex/plt/ax/_style/_rotate_labels_v01.py +73 -63
  407. scitex/plt/ax/_style/_set_log_scale.py +120 -85
  408. scitex/plt/ax/_style/_set_meta.py +99 -76
  409. scitex/plt/ax/_style/_set_supxyt.py +33 -16
  410. scitex/plt/ax/_style/_set_xyt.py +27 -18
  411. scitex/plt/ax/_style/_share_axes.py +15 -5
  412. scitex/plt/ax/_style/_show_spines.py +58 -57
  413. scitex/plt/ax/_style/_style_barplot.py +1 -1
  414. scitex/plt/ax/_style/_style_boxplot.py +25 -14
  415. scitex/plt/ax/_style/_style_errorbar.py +0 -0
  416. scitex/plt/ax/_style/_style_scatter.py +1 -1
  417. scitex/plt/ax/_style/_style_suptitles.py +3 -3
  418. scitex/plt/ax/_style/_style_violinplot.py +8 -2
  419. scitex/plt/color/__init__.py +34 -2
  420. scitex/plt/color/_add_hue_col.py +1 -0
  421. scitex/plt/color/_colors.py +0 -1
  422. scitex/plt/color/_get_colors_from_conf_matap.py +3 -1
  423. scitex/plt/color/_vizualize_colors.py +0 -1
  424. scitex/plt/docs/FIGURE_ARCHITECTURE.md +315 -0
  425. scitex/plt/gallery/README.md +75 -0
  426. scitex/plt/gallery/__init__.py +29 -0
  427. scitex/plt/gallery/_generate.py +153 -0
  428. scitex/plt/gallery/_plots.py +594 -0
  429. scitex/plt/gallery/_registry.py +153 -0
  430. scitex/plt/styles/__init__.py +9 -9
  431. scitex/plt/styles/_plot_defaults.py +62 -61
  432. scitex/plt/styles/_plot_postprocess.py +126 -77
  433. scitex/plt/styles/_style_loader.py +0 -0
  434. scitex/plt/styles/presets.py +43 -18
  435. scitex/plt/templates/research-master/scitex/vis/gallery/area/fill_between.json +110 -0
  436. scitex/plt/templates/research-master/scitex/vis/gallery/area/fill_betweenx.json +88 -0
  437. scitex/plt/templates/research-master/scitex/vis/gallery/area/stx_fill_between.json +103 -0
  438. scitex/plt/templates/research-master/scitex/vis/gallery/area/stx_fillv.json +106 -0
  439. scitex/plt/templates/research-master/scitex/vis/gallery/categorical/bar.json +92 -0
  440. scitex/plt/templates/research-master/scitex/vis/gallery/categorical/barh.json +92 -0
  441. scitex/plt/templates/research-master/scitex/vis/gallery/categorical/boxplot.json +92 -0
  442. scitex/plt/templates/research-master/scitex/vis/gallery/categorical/stx_bar.json +84 -0
  443. scitex/plt/templates/research-master/scitex/vis/gallery/categorical/stx_barh.json +84 -0
  444. scitex/plt/templates/research-master/scitex/vis/gallery/categorical/stx_box.json +83 -0
  445. scitex/plt/templates/research-master/scitex/vis/gallery/categorical/stx_boxplot.json +93 -0
  446. scitex/plt/templates/research-master/scitex/vis/gallery/categorical/stx_violin.json +91 -0
  447. scitex/plt/templates/research-master/scitex/vis/gallery/categorical/stx_violinplot.json +91 -0
  448. scitex/plt/templates/research-master/scitex/vis/gallery/categorical/violinplot.json +91 -0
  449. scitex/plt/templates/research-master/scitex/vis/gallery/contour/contour.json +97 -0
  450. scitex/plt/templates/research-master/scitex/vis/gallery/contour/contourf.json +98 -0
  451. scitex/plt/templates/research-master/scitex/vis/gallery/contour/stx_contour.json +84 -0
  452. scitex/plt/templates/research-master/scitex/vis/gallery/distribution/hist.json +101 -0
  453. scitex/plt/templates/research-master/scitex/vis/gallery/distribution/hist2d.json +96 -0
  454. scitex/plt/templates/research-master/scitex/vis/gallery/distribution/stx_ecdf.json +95 -0
  455. scitex/plt/templates/research-master/scitex/vis/gallery/distribution/stx_joyplot.json +95 -0
  456. scitex/plt/templates/research-master/scitex/vis/gallery/distribution/stx_kde.json +93 -0
  457. scitex/plt/templates/research-master/scitex/vis/gallery/grid/imshow.json +95 -0
  458. scitex/plt/templates/research-master/scitex/vis/gallery/grid/matshow.json +95 -0
  459. scitex/plt/templates/research-master/scitex/vis/gallery/grid/stx_conf_mat.json +83 -0
  460. scitex/plt/templates/research-master/scitex/vis/gallery/grid/stx_heatmap.json +92 -0
  461. scitex/plt/templates/research-master/scitex/vis/gallery/grid/stx_image.json +121 -0
  462. scitex/plt/templates/research-master/scitex/vis/gallery/grid/stx_imshow.json +84 -0
  463. scitex/plt/templates/research-master/scitex/vis/gallery/line/plot.json +110 -0
  464. scitex/plt/templates/research-master/scitex/vis/gallery/line/step.json +92 -0
  465. scitex/plt/templates/research-master/scitex/vis/gallery/line/stx_line.json +95 -0
  466. scitex/plt/templates/research-master/scitex/vis/gallery/line/stx_shaded_line.json +96 -0
  467. scitex/plt/templates/research-master/scitex/vis/gallery/scatter/hexbin.json +95 -0
  468. scitex/plt/templates/research-master/scitex/vis/gallery/scatter/scatter.json +95 -0
  469. scitex/plt/templates/research-master/scitex/vis/gallery/scatter/stem.json +92 -0
  470. scitex/plt/templates/research-master/scitex/vis/gallery/scatter/stx_scatter.json +84 -0
  471. scitex/plt/templates/research-master/scitex/vis/gallery/special/pie.json +94 -0
  472. scitex/plt/templates/research-master/scitex/vis/gallery/special/stx_raster.json +109 -0
  473. scitex/plt/templates/research-master/scitex/vis/gallery/special/stx_rectangle.json +108 -0
  474. scitex/plt/templates/research-master/scitex/vis/gallery/statistical/errorbar.json +93 -0
  475. scitex/plt/templates/research-master/scitex/vis/gallery/statistical/stx_errorbar.json +84 -0
  476. scitex/plt/templates/research-master/scitex/vis/gallery/statistical/stx_mean_ci.json +96 -0
  477. scitex/plt/templates/research-master/scitex/vis/gallery/statistical/stx_mean_std.json +96 -0
  478. scitex/plt/templates/research-master/scitex/vis/gallery/statistical/stx_median_iqr.json +96 -0
  479. scitex/plt/templates/research-master/scitex/vis/gallery/vector/quiver.json +99 -0
  480. scitex/plt/templates/research-master/scitex/vis/gallery/vector/streamplot.json +100 -0
  481. scitex/plt/utils/__init__.py +39 -2
  482. scitex/plt/utils/_close.py +8 -3
  483. scitex/plt/utils/_collect_figure_metadata.py +3033 -265
  484. scitex/plt/utils/_colorbar.py +15 -17
  485. scitex/plt/utils/_configure_mpl.py +22 -14
  486. scitex/plt/utils/_crop.py +60 -27
  487. scitex/plt/utils/_csv_column_naming.py +288 -0
  488. scitex/plt/utils/_dimension_viewer.py +7 -19
  489. scitex/plt/utils/_figure_from_axes_mm.py +70 -16
  490. scitex/plt/utils/_figure_mm.py +3 -2
  491. scitex/plt/utils/_get_actual_font.py +5 -4
  492. scitex/plt/utils/_histogram_utils.py +52 -48
  493. scitex/plt/utils/_is_valid_axis.py +19 -13
  494. scitex/plt/utils/_mk_colorbar.py +3 -3
  495. scitex/plt/utils/_scientific_captions.py +202 -139
  496. scitex/plt/utils/_scitex_config.py +98 -98
  497. scitex/plt/utils/_units.py +0 -0
  498. scitex/plt/utils/metadata/__init__.py +36 -0
  499. scitex/plt/utils/metadata/_artist_extraction.py +119 -0
  500. scitex/plt/utils/metadata/_axes_metadata.py +93 -0
  501. scitex/plt/utils/metadata/_collection_artists.py +292 -0
  502. scitex/plt/utils/metadata/_core.py +208 -0
  503. scitex/plt/utils/metadata/_csv_column_extraction.py +186 -0
  504. scitex/plt/utils/metadata/_csv_hash.py +115 -0
  505. scitex/plt/utils/metadata/_csv_verification.py +95 -0
  506. scitex/plt/utils/metadata/_data_linkage.py +263 -0
  507. scitex/plt/utils/metadata/_dimensions.py +239 -0
  508. scitex/plt/utils/metadata/_figure_metadata.py +58 -0
  509. scitex/plt/utils/metadata/_image_text_artists.py +168 -0
  510. scitex/plt/utils/metadata/_label_parsing.py +82 -0
  511. scitex/plt/utils/metadata/_legend_extraction.py +120 -0
  512. scitex/plt/utils/metadata/_line_artists.py +367 -0
  513. scitex/plt/utils/metadata/_line_semantic_handling.py +173 -0
  514. scitex/plt/utils/metadata/_patch_artists.py +211 -0
  515. scitex/plt/utils/metadata/_plot_content.py +26 -0
  516. scitex/plt/utils/metadata/_plot_type_detection.py +184 -0
  517. scitex/plt/utils/metadata/_precision.py +134 -0
  518. scitex/plt/utils/metadata/_precision_config.py +68 -0
  519. scitex/plt/utils/metadata/_precision_sections.py +211 -0
  520. scitex/plt/utils/metadata/_recipe_extraction.py +267 -0
  521. scitex/plt/utils/metadata/_style_parsing.py +174 -0
  522. scitex/repro/_RandomStateManager.py +33 -38
  523. scitex/repro/__init__.py +16 -7
  524. scitex/repro/_gen_ID.py +7 -9
  525. scitex/repro/_gen_timestamp.py +7 -6
  526. scitex/repro/_hash_array.py +8 -12
  527. scitex/reproduce/__init__.py +1 -1
  528. scitex/resource/_get_processor_usages.py +3 -1
  529. scitex/resource/_log_processor_usages.py +3 -1
  530. scitex/rng/__init__.py +1 -1
  531. scitex/schema/README.md +178 -0
  532. scitex/schema/__init__.py +144 -0
  533. scitex/schema/_canvas.py +444 -0
  534. scitex/schema/_stats.py +762 -0
  535. scitex/schema/_validation.py +590 -0
  536. scitex/scholar/.legacy/Scholar.py +5 -12
  537. scitex/scholar/.legacy/_Scholar.py +66 -99
  538. scitex/scholar/.legacy/_ScholarAPI.py +75 -66
  539. scitex/scholar/.legacy/_tmp/search_engine/_BaseSearchEngine.py +3 -3
  540. scitex/scholar/.legacy/_tmp/search_engine/_UnifiedSearcher.py +4 -9
  541. scitex/scholar/.legacy/_tmp/search_engine/__init__.py +14 -21
  542. scitex/scholar/.legacy/_tmp/search_engine/local/_LocalSearchEngine.py +40 -37
  543. scitex/scholar/.legacy/_tmp/search_engine/local/_VectorSearchEngine.py +31 -28
  544. scitex/scholar/.legacy/_tmp/search_engine/web/_ArxivSearchEngine.py +74 -65
  545. scitex/scholar/.legacy/_tmp/search_engine/web/_CrossRefSearchEngine.py +122 -116
  546. scitex/scholar/.legacy/_tmp/search_engine/web/_GoogleScholarSearchEngine.py +65 -59
  547. scitex/scholar/.legacy/_tmp/search_engine/web/_PubMedSearchEngine.py +121 -107
  548. scitex/scholar/.legacy/_tmp/search_engine/web/_SemanticScholarSearchEngine.py +5 -12
  549. scitex/scholar/.legacy/database/_DatabaseEntry.py +49 -45
  550. scitex/scholar/.legacy/database/_DatabaseIndex.py +131 -94
  551. scitex/scholar/.legacy/database/_LibraryManager.py +65 -63
  552. scitex/scholar/.legacy/database/_PaperDatabase.py +138 -124
  553. scitex/scholar/.legacy/database/_ScholarDatabaseIntegration.py +14 -36
  554. scitex/scholar/.legacy/database/_StorageIntegratedDB.py +192 -156
  555. scitex/scholar/.legacy/database/_ZoteroCompatibleDB.py +300 -237
  556. scitex/scholar/.legacy/database/__init__.py +2 -1
  557. scitex/scholar/.legacy/database/manage.py +92 -84
  558. scitex/scholar/.legacy/lookup/_LookupIndex.py +157 -101
  559. scitex/scholar/.legacy/lookup/__init__.py +2 -1
  560. scitex/scholar/.legacy/metadata/doi/batch/_MetadataHandlerForBatchDOIResolution.py +4 -9
  561. scitex/scholar/.legacy/metadata/doi/batch/_ProgressManagerForBatchDOIResolution.py +10 -23
  562. scitex/scholar/.legacy/metadata/doi/batch/_SourceStatsManagerForBatchDOIResolution.py +4 -9
  563. scitex/scholar/.legacy/metadata/doi/batch/__init__.py +3 -1
  564. scitex/scholar/.legacy/metadata/doi/resolvers/_BatchDOIResolver.py +10 -25
  565. scitex/scholar/.legacy/metadata/doi/resolvers/_BibTeXDOIResolver.py +19 -49
  566. scitex/scholar/.legacy/metadata/doi/resolvers/_DOIResolver.py +1 -0
  567. scitex/scholar/.legacy/metadata/doi/resolvers/_SingleDOIResolver.py +8 -20
  568. scitex/scholar/.legacy/metadata/doi/sources/.combined-SemanticScholarSource/_SemanticScholarSource.py +37 -35
  569. scitex/scholar/.legacy/metadata/doi/sources/.combined-SemanticScholarSource/_SemanticScholarSourceEnhanced.py +49 -37
  570. scitex/scholar/.legacy/metadata/doi/sources/_ArXivSource.py +11 -30
  571. scitex/scholar/.legacy/metadata/doi/sources/_BaseDOISource.py +19 -47
  572. scitex/scholar/.legacy/metadata/doi/sources/_CrossRefLocalSource.py +1 -0
  573. scitex/scholar/.legacy/metadata/doi/sources/_CrossRefSource.py +12 -33
  574. scitex/scholar/.legacy/metadata/doi/sources/_OpenAlexSource.py +8 -20
  575. scitex/scholar/.legacy/metadata/doi/sources/_PubMedSource.py +10 -27
  576. scitex/scholar/.legacy/metadata/doi/sources/_SemanticScholarSource.py +11 -29
  577. scitex/scholar/.legacy/metadata/doi/sources/_SourceManager.py +8 -21
  578. scitex/scholar/.legacy/metadata/doi/sources/_SourceResolutionStrategy.py +24 -55
  579. scitex/scholar/.legacy/metadata/doi/sources/_SourceRotationManager.py +8 -21
  580. scitex/scholar/.legacy/metadata/doi/sources/_URLDOISource.py +9 -16
  581. scitex/scholar/.legacy/metadata/doi/sources/_UnifiedSource.py +8 -22
  582. scitex/scholar/.legacy/metadata/doi/sources/__init__.py +1 -0
  583. scitex/scholar/.legacy/metadata/doi/utils/_PubMedConverter.py +4 -8
  584. scitex/scholar/.legacy/metadata/doi/utils/_RateLimitHandler.py +17 -43
  585. scitex/scholar/.legacy/metadata/doi/utils/_TextNormalizer.py +8 -18
  586. scitex/scholar/.legacy/metadata/doi/utils/_URLDOIExtractor.py +4 -8
  587. scitex/scholar/.legacy/metadata/doi/utils/__init__.py +1 -0
  588. scitex/scholar/.legacy/metadata/doi/utils/_to_complete_metadata_structure.py +1 -0
  589. scitex/scholar/.legacy/metadata/enrichment/_LibraryEnricher.py +2 -3
  590. scitex/scholar/.legacy/metadata/enrichment/enrichers/_ImpactFactorEnricher.py +6 -12
  591. scitex/scholar/.legacy/metadata/enrichment/enrichers/_SmartEnricher.py +5 -10
  592. scitex/scholar/.legacy/metadata/enrichment/sources/_UnifiedMetadataSource.py +4 -5
  593. scitex/scholar/.legacy/metadata/query_to_full_meta_json.py +8 -12
  594. scitex/scholar/.legacy/metadata/urls/_URLMetadataHandler.py +3 -3
  595. scitex/scholar/.legacy/metadata/urls/_ZoteroTranslatorRunner.py +15 -21
  596. scitex/scholar/.legacy/metadata/urls/__init__.py +3 -3
  597. scitex/scholar/.legacy/metadata/urls/_finder.py +4 -6
  598. scitex/scholar/.legacy/metadata/urls/_handler.py +7 -15
  599. scitex/scholar/.legacy/metadata/urls/_resolver.py +6 -12
  600. scitex/scholar/.legacy/search/_Embedder.py +74 -69
  601. scitex/scholar/.legacy/search/_SemanticSearch.py +91 -90
  602. scitex/scholar/.legacy/search/_SemanticSearchEngine.py +104 -109
  603. scitex/scholar/.legacy/search/_UnifiedSearcher.py +530 -471
  604. scitex/scholar/.legacy/search/_VectorDatabase.py +111 -92
  605. scitex/scholar/.legacy/search/__init__.py +1 -0
  606. scitex/scholar/.legacy/storage/_EnhancedStorageManager.py +182 -154
  607. scitex/scholar/.legacy/storage/__init__.py +2 -1
  608. scitex/scholar/__init__.py +0 -2
  609. scitex/scholar/__main__.py +1 -3
  610. scitex/scholar/auth/ScholarAuthManager.py +13 -36
  611. scitex/scholar/auth/core/AuthenticationGateway.py +15 -29
  612. scitex/scholar/auth/core/BrowserAuthenticator.py +22 -57
  613. scitex/scholar/auth/core/StrategyResolver.py +10 -27
  614. scitex/scholar/auth/core/__init__.py +5 -1
  615. scitex/scholar/auth/gateway/_OpenURLLinkFinder.py +11 -21
  616. scitex/scholar/auth/gateway/_OpenURLResolver.py +10 -18
  617. scitex/scholar/auth/gateway/_resolve_functions.py +3 -3
  618. scitex/scholar/auth/providers/BaseAuthenticator.py +1 -0
  619. scitex/scholar/auth/providers/EZProxyAuthenticator.py +7 -14
  620. scitex/scholar/auth/providers/OpenAthensAuthenticator.py +29 -57
  621. scitex/scholar/auth/providers/ShibbolethAuthenticator.py +87 -73
  622. scitex/scholar/auth/session/AuthCacheManager.py +12 -22
  623. scitex/scholar/auth/session/SessionManager.py +4 -6
  624. scitex/scholar/auth/sso/BaseSSOAutomator.py +13 -19
  625. scitex/scholar/auth/sso/OpenAthensSSOAutomator.py +16 -45
  626. scitex/scholar/auth/sso/SSOAutomator.py +8 -15
  627. scitex/scholar/auth/sso/UniversityOfMelbourneSSOAutomator.py +13 -23
  628. scitex/scholar/browser/ScholarBrowserManager.py +31 -56
  629. scitex/scholar/browser/__init__.py +1 -0
  630. scitex/scholar/browser/utils/click_and_wait.py +3 -4
  631. scitex/scholar/browser/utils/close_unwanted_pages.py +4 -7
  632. scitex/scholar/browser/utils/wait_redirects.py +15 -40
  633. scitex/scholar/citation_graph/__init__.py +0 -0
  634. scitex/scholar/citation_graph/builder.py +3 -7
  635. scitex/scholar/citation_graph/database.py +4 -11
  636. scitex/scholar/citation_graph/example.py +5 -10
  637. scitex/scholar/citation_graph/models.py +0 -0
  638. scitex/scholar/cli/_url_utils.py +1 -1
  639. scitex/scholar/cli/chrome.py +5 -3
  640. scitex/scholar/cli/download_pdf.py +13 -14
  641. scitex/scholar/cli/handlers/bibtex_handler.py +4 -12
  642. scitex/scholar/cli/handlers/doi_handler.py +1 -3
  643. scitex/scholar/cli/handlers/project_handler.py +6 -20
  644. scitex/scholar/cli/open_browser.py +41 -39
  645. scitex/scholar/cli/open_browser_auto.py +31 -39
  646. scitex/scholar/cli/open_browser_monitored.py +27 -24
  647. scitex/scholar/config/ScholarConfig.py +5 -8
  648. scitex/scholar/config/__init__.py +1 -0
  649. scitex/scholar/config/core/_CascadeConfig.py +3 -3
  650. scitex/scholar/config/core/_PathManager.py +16 -28
  651. scitex/scholar/core/Paper.py +79 -78
  652. scitex/scholar/core/Papers.py +16 -27
  653. scitex/scholar/core/Scholar.py +98 -229
  654. scitex/scholar/core/journal_normalizer.py +52 -49
  655. scitex/scholar/core/oa_cache.py +27 -23
  656. scitex/scholar/core/open_access.py +17 -8
  657. scitex/scholar/docs/template.py +4 -3
  658. scitex/scholar/docs/to_claude/examples/example-python-project-scitex/scripts/mnist/clf_svm.py +0 -0
  659. scitex/scholar/docs/to_claude/examples/example-python-project-scitex/scripts/mnist/download.py +0 -0
  660. scitex/scholar/docs/to_claude/examples/example-python-project-scitex/scripts/mnist/plot_conf_mat.py +0 -0
  661. scitex/scholar/docs/to_claude/examples/example-python-project-scitex/scripts/mnist/plot_digits.py +0 -0
  662. scitex/scholar/docs/to_claude/examples/example-python-project-scitex/scripts/mnist/plot_umap_space.py +0 -0
  663. scitex/scholar/examples/00_config.py +10 -9
  664. scitex/scholar/examples/01_auth.py +3 -0
  665. scitex/scholar/examples/02_browser.py +14 -10
  666. scitex/scholar/examples/03_01-engine.py +3 -0
  667. scitex/scholar/examples/03_02-engine-for-bibtex.py +4 -3
  668. scitex/scholar/examples/04_01-url.py +9 -9
  669. scitex/scholar/examples/04_02-url-for-bibtex.py +7 -3
  670. scitex/scholar/examples/04_02-url-for-dois.py +87 -97
  671. scitex/scholar/examples/05_download_pdf.py +10 -4
  672. scitex/scholar/examples/06_find_and_download.py +6 -6
  673. scitex/scholar/examples/06_parse_bibtex.py +17 -17
  674. scitex/scholar/examples/07_storage_integration.py +6 -9
  675. scitex/scholar/examples/99_fullpipeline-for-bibtex.py +14 -15
  676. scitex/scholar/examples/99_fullpipeline-for-one-entry.py +31 -23
  677. scitex/scholar/examples/99_maintenance.py +3 -0
  678. scitex/scholar/examples/dev.py +2 -3
  679. scitex/scholar/examples/zotero_integration.py +11 -18
  680. scitex/scholar/impact_factor/ImpactFactorEngine.py +7 -9
  681. scitex/scholar/impact_factor/estimation/__init__.py +4 -4
  682. scitex/scholar/impact_factor/estimation/core/__init__.py +3 -7
  683. scitex/scholar/impact_factor/estimation/core/cache_manager.py +223 -211
  684. scitex/scholar/impact_factor/estimation/core/calculator.py +165 -131
  685. scitex/scholar/impact_factor/estimation/core/journal_matcher.py +217 -172
  686. scitex/scholar/impact_factor/jcr/ImpactFactorJCREngine.py +6 -14
  687. scitex/scholar/impact_factor/jcr/build_database.py +4 -3
  688. scitex/scholar/integration/base.py +9 -17
  689. scitex/scholar/integration/mendeley/exporter.py +2 -4
  690. scitex/scholar/integration/mendeley/importer.py +3 -3
  691. scitex/scholar/integration/mendeley/linker.py +3 -3
  692. scitex/scholar/integration/mendeley/mapper.py +9 -6
  693. scitex/scholar/integration/zotero/__main__.py +26 -43
  694. scitex/scholar/integration/zotero/exporter.py +15 -11
  695. scitex/scholar/integration/zotero/importer.py +12 -10
  696. scitex/scholar/integration/zotero/linker.py +8 -12
  697. scitex/scholar/integration/zotero/mapper.py +17 -12
  698. scitex/scholar/metadata_engines/.combined-SemanticScholarSource/_SemanticScholarSource.py +37 -35
  699. scitex/scholar/metadata_engines/.combined-SemanticScholarSource/_SemanticScholarSourceEnhanced.py +47 -35
  700. scitex/scholar/metadata_engines/ScholarEngine.py +21 -43
  701. scitex/scholar/metadata_engines/__init__.py +1 -0
  702. scitex/scholar/metadata_engines/individual/ArXivEngine.py +15 -37
  703. scitex/scholar/metadata_engines/individual/CrossRefEngine.py +15 -42
  704. scitex/scholar/metadata_engines/individual/CrossRefLocalEngine.py +24 -45
  705. scitex/scholar/metadata_engines/individual/OpenAlexEngine.py +11 -21
  706. scitex/scholar/metadata_engines/individual/PubMedEngine.py +10 -27
  707. scitex/scholar/metadata_engines/individual/SemanticScholarEngine.py +28 -35
  708. scitex/scholar/metadata_engines/individual/URLDOIEngine.py +11 -22
  709. scitex/scholar/metadata_engines/individual/_BaseDOIEngine.py +20 -49
  710. scitex/scholar/metadata_engines/utils/_PubMedConverter.py +4 -8
  711. scitex/scholar/metadata_engines/utils/_URLDOIExtractor.py +5 -10
  712. scitex/scholar/metadata_engines/utils/__init__.py +2 -0
  713. scitex/scholar/metadata_engines/utils/_metadata2bibtex.py +3 -0
  714. scitex/scholar/metadata_engines/utils/_standardize_metadata.py +2 -3
  715. scitex/scholar/pdf_download/ScholarPDFDownloader.py +25 -37
  716. scitex/scholar/pdf_download/strategies/chrome_pdf_viewer.py +11 -19
  717. scitex/scholar/pdf_download/strategies/direct_download.py +5 -9
  718. scitex/scholar/pdf_download/strategies/manual_download_fallback.py +3 -3
  719. scitex/scholar/pdf_download/strategies/manual_download_utils.py +6 -13
  720. scitex/scholar/pdf_download/strategies/open_access_download.py +49 -31
  721. scitex/scholar/pdf_download/strategies/response_body.py +8 -19
  722. scitex/scholar/pipelines/ScholarPipelineBibTeX.py +9 -18
  723. scitex/scholar/pipelines/ScholarPipelineMetadataParallel.py +25 -26
  724. scitex/scholar/pipelines/ScholarPipelineMetadataSingle.py +62 -23
  725. scitex/scholar/pipelines/ScholarPipelineParallel.py +13 -30
  726. scitex/scholar/pipelines/ScholarPipelineSearchParallel.py +299 -220
  727. scitex/scholar/pipelines/ScholarPipelineSearchSingle.py +202 -165
  728. scitex/scholar/pipelines/ScholarPipelineSingle.py +25 -51
  729. scitex/scholar/pipelines/SearchQueryParser.py +55 -55
  730. scitex/scholar/search_engines/ScholarSearchEngine.py +31 -27
  731. scitex/scholar/search_engines/_BaseSearchEngine.py +20 -23
  732. scitex/scholar/search_engines/individual/ArXivSearchEngine.py +53 -35
  733. scitex/scholar/search_engines/individual/CrossRefSearchEngine.py +47 -40
  734. scitex/scholar/search_engines/individual/OpenAlexSearchEngine.py +55 -50
  735. scitex/scholar/search_engines/individual/PubMedSearchEngine.py +8 -10
  736. scitex/scholar/search_engines/individual/SemanticScholarSearchEngine.py +55 -49
  737. scitex/scholar/storage/BibTeXHandler.py +150 -95
  738. scitex/scholar/storage/PaperIO.py +3 -6
  739. scitex/scholar/storage/ScholarLibrary.py +70 -49
  740. scitex/scholar/storage/_DeduplicationManager.py +52 -25
  741. scitex/scholar/storage/_LibraryCacheManager.py +19 -46
  742. scitex/scholar/storage/_LibraryManager.py +65 -175
  743. scitex/scholar/url_finder/ScholarURLFinder.py +9 -25
  744. scitex/scholar/url_finder/strategies/find_pdf_urls_by_direct_links.py +1 -1
  745. scitex/scholar/url_finder/strategies/find_pdf_urls_by_href.py +6 -10
  746. scitex/scholar/url_finder/strategies/find_pdf_urls_by_navigation.py +4 -6
  747. scitex/scholar/url_finder/strategies/find_pdf_urls_by_publisher_patterns.py +8 -15
  748. scitex/scholar/url_finder/strategies/find_pdf_urls_by_zotero_translators.py +3 -3
  749. scitex/scholar/url_finder/strategies/find_supplementary_urls_by_href.py +3 -3
  750. scitex/scholar/url_finder/translators/core/patterns.py +6 -4
  751. scitex/scholar/url_finder/translators/core/registry.py +6 -9
  752. scitex/scholar/url_finder/translators/individual/BOFiP_Impots.py +60 -52
  753. scitex/scholar/url_finder/translators/individual/Baidu_Scholar.py +54 -62
  754. scitex/scholar/url_finder/translators/individual/Bangkok_Post.py +38 -44
  755. scitex/scholar/url_finder/translators/individual/Baruch_Foundation.py +43 -47
  756. scitex/scholar/url_finder/translators/individual/Beobachter.py +46 -50
  757. scitex/scholar/url_finder/translators/individual/Bezneng_Gajit.py +37 -41
  758. scitex/scholar/url_finder/translators/individual/BibLaTeX.py +59 -52
  759. scitex/scholar/url_finder/translators/individual/BibTeX.py +83 -79
  760. scitex/scholar/url_finder/translators/individual/Biblio_com.py +48 -51
  761. scitex/scholar/url_finder/translators/individual/Bibliontology_RDF.py +58 -56
  762. scitex/scholar/url_finder/translators/individual/Camara_Brasileira_do_Livro_ISBN.py +102 -99
  763. scitex/scholar/url_finder/translators/individual/CanLII.py +49 -43
  764. scitex/scholar/url_finder/translators/individual/Canada_com.py +36 -40
  765. scitex/scholar/url_finder/translators/individual/Canadian_Letters_and_Images.py +43 -43
  766. scitex/scholar/url_finder/translators/individual/Canadiana_ca.py +77 -66
  767. scitex/scholar/url_finder/translators/individual/Cascadilla_Proceedings_Project.py +68 -62
  768. scitex/scholar/url_finder/translators/individual/Central_and_Eastern_European_Online_Library_Journals.py +60 -60
  769. scitex/scholar/url_finder/translators/individual/Champlain_Society_Collection.py +63 -61
  770. scitex/scholar/url_finder/translators/individual/Chicago_Journal_of_Theoretical_Computer_Science.py +74 -58
  771. scitex/scholar/url_finder/translators/individual/Christian_Science_Monitor.py +32 -38
  772. scitex/scholar/url_finder/translators/individual/Columbia_University_Press.py +51 -47
  773. scitex/scholar/url_finder/translators/individual/Common_Place.py +66 -57
  774. scitex/scholar/url_finder/translators/individual/Cornell_LII.py +66 -62
  775. scitex/scholar/url_finder/translators/individual/Cornell_University_Press.py +38 -45
  776. scitex/scholar/url_finder/translators/individual/CourtListener.py +52 -56
  777. scitex/scholar/url_finder/translators/individual/DAI_Zenon.py +53 -54
  778. scitex/scholar/url_finder/translators/individual/access_medicine.py +27 -33
  779. scitex/scholar/url_finder/translators/individual/acm.py +1 -1
  780. scitex/scholar/url_finder/translators/individual/acm_digital_library.py +93 -63
  781. scitex/scholar/url_finder/translators/individual/airiti.py +3 -1
  782. scitex/scholar/url_finder/translators/individual/aosic.py +3 -1
  783. scitex/scholar/url_finder/translators/individual/archive_ouverte_aosic.py +3 -1
  784. scitex/scholar/url_finder/translators/individual/archive_ouverte_en_sciences_de_l_information_et_de_la_communication___aosic_.py +6 -2
  785. scitex/scholar/url_finder/translators/individual/artforum.py +35 -27
  786. scitex/scholar/url_finder/translators/individual/arxiv.py +1 -1
  787. scitex/scholar/url_finder/translators/individual/arxiv_org.py +8 -4
  788. scitex/scholar/url_finder/translators/individual/atlanta_journal_constitution.py +22 -18
  789. scitex/scholar/url_finder/translators/individual/atypon_journals.py +19 -11
  790. scitex/scholar/url_finder/translators/individual/austlii_and_nzlii.py +48 -44
  791. scitex/scholar/url_finder/translators/individual/australian_dictionary_of_biography.py +21 -17
  792. scitex/scholar/url_finder/translators/individual/bailii.py +22 -19
  793. scitex/scholar/url_finder/translators/individual/bbc.py +46 -42
  794. scitex/scholar/url_finder/translators/individual/bbc_genome.py +37 -25
  795. scitex/scholar/url_finder/translators/individual/biblioteca_nacional_de_maestros.py +24 -20
  796. scitex/scholar/url_finder/translators/individual/bibliotheque_archives_nationale_quebec_pistard.py +42 -43
  797. scitex/scholar/url_finder/translators/individual/bibliotheque_archives_nationales_quebec.py +87 -81
  798. scitex/scholar/url_finder/translators/individual/bibliotheque_nationale_france.py +39 -37
  799. scitex/scholar/url_finder/translators/individual/bibsys.py +32 -28
  800. scitex/scholar/url_finder/translators/individual/bioconductor.py +58 -52
  801. scitex/scholar/url_finder/translators/individual/biomed_central.py +23 -15
  802. scitex/scholar/url_finder/translators/individual/biorxiv.py +26 -13
  803. scitex/scholar/url_finder/translators/individual/blogger.py +39 -43
  804. scitex/scholar/url_finder/translators/individual/bloomberg.py +48 -52
  805. scitex/scholar/url_finder/translators/individual/bloomsbury_food_library.py +37 -37
  806. scitex/scholar/url_finder/translators/individual/bluesky.py +30 -28
  807. scitex/scholar/url_finder/translators/individual/bnf_isbn.py +1 -1
  808. scitex/scholar/url_finder/translators/individual/bocc.py +66 -60
  809. scitex/scholar/url_finder/translators/individual/boe.py +52 -52
  810. scitex/scholar/url_finder/translators/individual/brill.py +3 -1
  811. scitex/scholar/url_finder/translators/individual/business_standard.py +36 -38
  812. scitex/scholar/url_finder/translators/individual/cabi_cab_abstracts.py +39 -41
  813. scitex/scholar/url_finder/translators/individual/cambridge.py +3 -1
  814. scitex/scholar/url_finder/translators/individual/cambridge_core.py +30 -24
  815. scitex/scholar/url_finder/translators/individual/caod.py +50 -46
  816. scitex/scholar/url_finder/translators/individual/cbc.py +91 -67
  817. scitex/scholar/url_finder/translators/individual/ccfr_bnf.py +49 -53
  818. scitex/scholar/url_finder/translators/individual/cia_world_factbook.py +43 -33
  819. scitex/scholar/url_finder/translators/individual/crossref_rest.py +208 -174
  820. scitex/scholar/url_finder/translators/individual/current_affairs.py +29 -35
  821. scitex/scholar/url_finder/translators/individual/dabi.py +70 -66
  822. scitex/scholar/url_finder/translators/individual/dagens_nyheter.py +3 -1
  823. scitex/scholar/url_finder/translators/individual/dagstuhl.py +10 -15
  824. scitex/scholar/url_finder/translators/individual/dar_almandumah.py +13 -9
  825. scitex/scholar/url_finder/translators/individual/dart_europe.py +19 -22
  826. scitex/scholar/url_finder/translators/individual/data_gov.py +2 -2
  827. scitex/scholar/url_finder/translators/individual/databrary.py +27 -28
  828. scitex/scholar/url_finder/translators/individual/datacite_json.py +152 -137
  829. scitex/scholar/url_finder/translators/individual/dataverse.py +68 -64
  830. scitex/scholar/url_finder/translators/individual/daum_news.py +38 -38
  831. scitex/scholar/url_finder/translators/individual/dblp.py +4 -8
  832. scitex/scholar/url_finder/translators/individual/dblp_computer_science_bibliography.py +8 -3
  833. scitex/scholar/url_finder/translators/individual/dbpia.py +5 -3
  834. scitex/scholar/url_finder/translators/individual/defense_technical_information_center.py +30 -28
  835. scitex/scholar/url_finder/translators/individual/delpher.py +102 -79
  836. scitex/scholar/url_finder/translators/individual/demographic_research.py +35 -31
  837. scitex/scholar/url_finder/translators/individual/denik_cz.py +58 -54
  838. scitex/scholar/url_finder/translators/individual/depatisnet.py +7 -10
  839. scitex/scholar/url_finder/translators/individual/der_freitag.py +81 -66
  840. scitex/scholar/url_finder/translators/individual/der_spiegel.py +56 -54
  841. scitex/scholar/url_finder/translators/individual/digibib_net.py +3 -1
  842. scitex/scholar/url_finder/translators/individual/digizeitschriften.py +3 -1
  843. scitex/scholar/url_finder/translators/individual/dpla.py +13 -14
  844. scitex/scholar/url_finder/translators/individual/dspace.py +2 -2
  845. scitex/scholar/url_finder/translators/individual/ebrary.py +3 -1
  846. scitex/scholar/url_finder/translators/individual/ebscohost.py +3 -1
  847. scitex/scholar/url_finder/translators/individual/electronic_colloquium_on_computational_complexity.py +3 -1
  848. scitex/scholar/url_finder/translators/individual/elife.py +3 -1
  849. scitex/scholar/url_finder/translators/individual/elsevier_health_journals.py +3 -1
  850. scitex/scholar/url_finder/translators/individual/emerald.py +3 -1
  851. scitex/scholar/url_finder/translators/individual/emerald_insight.py +3 -1
  852. scitex/scholar/url_finder/translators/individual/epicurious.py +3 -1
  853. scitex/scholar/url_finder/translators/individual/eurogamerusgamer.py +3 -1
  854. scitex/scholar/url_finder/translators/individual/fachportal_padagogik.py +3 -1
  855. scitex/scholar/url_finder/translators/individual/frontiers.py +1 -1
  856. scitex/scholar/url_finder/translators/individual/gale_databases.py +3 -1
  857. scitex/scholar/url_finder/translators/individual/gms_german_medical_science.py +6 -2
  858. scitex/scholar/url_finder/translators/individual/ieee_computer_society.py +6 -2
  859. scitex/scholar/url_finder/translators/individual/ieee_xplore.py +41 -35
  860. scitex/scholar/url_finder/translators/individual/inter_research_science_center.py +6 -2
  861. scitex/scholar/url_finder/translators/individual/jisc_historical_texts.py +3 -1
  862. scitex/scholar/url_finder/translators/individual/jstor.py +14 -12
  863. scitex/scholar/url_finder/translators/individual/korean_national_library.py +3 -1
  864. scitex/scholar/url_finder/translators/individual/la_times.py +3 -1
  865. scitex/scholar/url_finder/translators/individual/landesbibliographie_baden_wurttemberg.py +3 -1
  866. scitex/scholar/url_finder/translators/individual/legislative_insight.py +3 -1
  867. scitex/scholar/url_finder/translators/individual/libraries_tasmania.py +3 -1
  868. scitex/scholar/url_finder/translators/individual/library_catalog__koha_.py +3 -1
  869. scitex/scholar/url_finder/translators/individual/lingbuzz.py +2 -2
  870. scitex/scholar/url_finder/translators/individual/max_planck_institute_for_the_history_of_science_virtual_laboratory_library.py +3 -1
  871. scitex/scholar/url_finder/translators/individual/mdpi.py +12 -6
  872. scitex/scholar/url_finder/translators/individual/microbiology_society_journals.py +3 -1
  873. scitex/scholar/url_finder/translators/individual/midas_journals.py +3 -1
  874. scitex/scholar/url_finder/translators/individual/nagoya_university_opac.py +3 -1
  875. scitex/scholar/url_finder/translators/individual/nature_publishing_group.py +32 -19
  876. scitex/scholar/url_finder/translators/individual/ntsb_accident_reports.py +3 -1
  877. scitex/scholar/url_finder/translators/individual/openedition_journals.py +8 -4
  878. scitex/scholar/url_finder/translators/individual/orcid.py +16 -15
  879. scitex/scholar/url_finder/translators/individual/oxford.py +25 -19
  880. scitex/scholar/url_finder/translators/individual/oxford_dictionaries_premium.py +3 -1
  881. scitex/scholar/url_finder/translators/individual/ozon_ru.py +3 -1
  882. scitex/scholar/url_finder/translators/individual/plos.py +9 -12
  883. scitex/scholar/url_finder/translators/individual/polygon.py +3 -1
  884. scitex/scholar/url_finder/translators/individual/primo.py +3 -1
  885. scitex/scholar/url_finder/translators/individual/project_muse.py +3 -1
  886. scitex/scholar/url_finder/translators/individual/pubfactory_journals.py +3 -1
  887. scitex/scholar/url_finder/translators/individual/pubmed.py +71 -65
  888. scitex/scholar/url_finder/translators/individual/pubmed_central.py +8 -6
  889. scitex/scholar/url_finder/translators/individual/rechtspraak_nl.py +3 -1
  890. scitex/scholar/url_finder/translators/individual/sage_journals.py +25 -17
  891. scitex/scholar/url_finder/translators/individual/sciencedirect.py +36 -17
  892. scitex/scholar/url_finder/translators/individual/semantics_visual_library.py +3 -1
  893. scitex/scholar/url_finder/translators/individual/silverchair.py +70 -52
  894. scitex/scholar/url_finder/translators/individual/sora.py +3 -1
  895. scitex/scholar/url_finder/translators/individual/springer.py +15 -11
  896. scitex/scholar/url_finder/translators/individual/ssrn.py +3 -3
  897. scitex/scholar/url_finder/translators/individual/stanford_encyclopedia_of_philosophy.py +3 -1
  898. scitex/scholar/url_finder/translators/individual/superlib.py +3 -1
  899. scitex/scholar/url_finder/translators/individual/treesearch.py +3 -1
  900. scitex/scholar/url_finder/translators/individual/university_of_chicago_press_books.py +3 -1
  901. scitex/scholar/url_finder/translators/individual/vlex.py +3 -1
  902. scitex/scholar/url_finder/translators/individual/web_of_science.py +3 -1
  903. scitex/scholar/url_finder/translators/individual/web_of_science_nextgen.py +3 -1
  904. scitex/scholar/url_finder/translators/individual/wiley.py +31 -25
  905. scitex/scholar/url_finder/translators/individual/wilson_center_digital_archive.py +3 -1
  906. scitex/scholar/utils/bibtex/_parse_bibtex.py +3 -3
  907. scitex/scholar/utils/cleanup/_cleanup_scholar_processes.py +5 -9
  908. scitex/scholar/utils/text/_TextNormalizer.py +249 -176
  909. scitex/scholar/utils/validation/DOIValidator.py +31 -28
  910. scitex/scholar/utils/validation/__init__.py +0 -0
  911. scitex/scholar/utils/validation/validate_library_dois.py +61 -57
  912. scitex/scholar/zotero/__init__.py +1 -1
  913. scitex/security/cli.py +7 -20
  914. scitex/security/github.py +45 -32
  915. scitex/session/__init__.py +8 -9
  916. scitex/session/_decorator.py +60 -41
  917. scitex/session/_lifecycle.py +39 -39
  918. scitex/session/_manager.py +24 -20
  919. scitex/sh/__init__.py +4 -3
  920. scitex/sh/_execute.py +10 -7
  921. scitex/sh/_security.py +3 -3
  922. scitex/sh/_types.py +2 -3
  923. scitex/stats/__init__.py +57 -6
  924. scitex/stats/_schema.py +42 -569
  925. scitex/stats/auto/__init__.py +188 -0
  926. scitex/stats/auto/_context.py +331 -0
  927. scitex/stats/auto/_formatting.py +679 -0
  928. scitex/stats/auto/_rules.py +901 -0
  929. scitex/stats/auto/_selector.py +554 -0
  930. scitex/stats/auto/_styles.py +721 -0
  931. scitex/stats/correct/__init__.py +4 -4
  932. scitex/stats/correct/_correct_bonferroni.py +43 -34
  933. scitex/stats/correct/_correct_fdr.py +14 -40
  934. scitex/stats/correct/_correct_fdr_.py +39 -46
  935. scitex/stats/correct/_correct_holm.py +14 -32
  936. scitex/stats/correct/_correct_sidak.py +36 -21
  937. scitex/stats/descriptive/_circular.py +20 -21
  938. scitex/stats/descriptive/_describe.py +19 -5
  939. scitex/stats/descriptive/_nan.py +5 -7
  940. scitex/stats/descriptive/_real.py +4 -3
  941. scitex/stats/effect_sizes/__init__.py +10 -11
  942. scitex/stats/effect_sizes/_cliffs_delta.py +35 -32
  943. scitex/stats/effect_sizes/_cohens_d.py +30 -31
  944. scitex/stats/effect_sizes/_epsilon_squared.py +19 -22
  945. scitex/stats/effect_sizes/_eta_squared.py +23 -27
  946. scitex/stats/effect_sizes/_prob_superiority.py +18 -21
  947. scitex/stats/posthoc/__init__.py +3 -3
  948. scitex/stats/posthoc/_dunnett.py +75 -55
  949. scitex/stats/posthoc/_games_howell.py +61 -43
  950. scitex/stats/posthoc/_tukey_hsd.py +42 -34
  951. scitex/stats/power/__init__.py +2 -2
  952. scitex/stats/power/_power.py +56 -56
  953. scitex/stats/tests/__init__.py +1 -1
  954. scitex/stats/tests/correlation/__init__.py +1 -1
  955. scitex/stats/tests/correlation/_test_pearson.py +28 -38
  956. scitex/stats/utils/__init__.py +14 -17
  957. scitex/stats/utils/_effect_size.py +85 -78
  958. scitex/stats/utils/_formatters.py +49 -43
  959. scitex/stats/utils/_normalizers.py +7 -14
  960. scitex/stats/utils/_power.py +56 -56
  961. scitex/str/__init__.py +1 -0
  962. scitex/str/_clean_path.py +3 -3
  963. scitex/str/_factor_out_digits.py +86 -58
  964. scitex/str/_format_plot_text.py +180 -111
  965. scitex/str/_latex.py +19 -19
  966. scitex/str/_latex_fallback.py +9 -10
  967. scitex/str/_parse.py +3 -6
  968. scitex/str/_print_debug.py +13 -13
  969. scitex/str/_printc.py +2 -0
  970. scitex/str/_search.py +3 -3
  971. scitex/template/.legacy/_clone_project.py +9 -13
  972. scitex/template/__init__.py +10 -2
  973. scitex/template/_clone_project.py +7 -2
  974. scitex/template/_copy.py +1 -0
  975. scitex/template/_customize.py +3 -6
  976. scitex/template/_git_strategy.py +2 -3
  977. scitex/template/_rename.py +1 -0
  978. scitex/template/clone_pip_project.py +6 -7
  979. scitex/template/clone_research.py +7 -10
  980. scitex/template/clone_singularity.py +6 -7
  981. scitex/template/clone_writer_directory.py +6 -7
  982. scitex/tex/_preview.py +26 -11
  983. scitex/tex/_to_vec.py +10 -7
  984. scitex/torch/__init__.py +11 -1
  985. scitex/types/_ArrayLike.py +2 -0
  986. scitex/types/_is_listed_X.py +3 -3
  987. scitex/units.py +110 -77
  988. scitex/utils/_compress_hdf5.py +3 -3
  989. scitex/utils/_email.py +8 -4
  990. scitex/utils/_notify.py +14 -8
  991. scitex/utils/_search.py +6 -6
  992. scitex/utils/_verify_scitex_format.py +17 -42
  993. scitex/utils/_verify_scitex_format_v01.py +12 -34
  994. scitex/utils/template.py +4 -3
  995. scitex/vis/README.md +246 -615
  996. scitex/vis/__init__.py +138 -78
  997. scitex/vis/backend/__init__.py +3 -3
  998. scitex/vis/backend/{export.py → _export.py} +1 -1
  999. scitex/vis/backend/{parser.py → _parser.py} +1 -3
  1000. scitex/vis/backend/{render.py → _render.py} +1 -1
  1001. scitex/vis/canvas.py +435 -0
  1002. scitex/vis/docs/CANVAS_ARCHITECTURE.md +307 -0
  1003. scitex/vis/editor/__init__.py +1 -1
  1004. scitex/vis/editor/_dearpygui_editor.py +1976 -0
  1005. scitex/vis/editor/_defaults.py +140 -110
  1006. scitex/vis/editor/_edit.py +90 -42
  1007. scitex/vis/editor/_flask_editor.py +37 -0
  1008. scitex/vis/editor/_mpl_editor.py +63 -48
  1009. scitex/vis/editor/_qt_editor.py +916 -0
  1010. scitex/vis/editor/_tkinter_editor.py +146 -89
  1011. scitex/vis/editor/flask_editor/__init__.py +21 -0
  1012. scitex/vis/editor/flask_editor/_bbox.py +529 -0
  1013. scitex/vis/editor/flask_editor/_core.py +168 -0
  1014. scitex/vis/editor/flask_editor/_plotter.py +567 -0
  1015. scitex/vis/editor/flask_editor/_renderer.py +393 -0
  1016. scitex/vis/editor/flask_editor/_utils.py +80 -0
  1017. scitex/vis/editor/flask_editor/templates/__init__.py +33 -0
  1018. scitex/vis/editor/flask_editor/templates/_html.py +513 -0
  1019. scitex/vis/editor/flask_editor/templates/_scripts.py +1261 -0
  1020. scitex/vis/editor/flask_editor/templates/_styles.py +739 -0
  1021. scitex/vis/io/__init__.py +84 -21
  1022. scitex/vis/io/_canvas.py +230 -0
  1023. scitex/vis/io/_data.py +208 -0
  1024. scitex/vis/io/_directory.py +205 -0
  1025. scitex/vis/io/_export.py +463 -0
  1026. scitex/vis/io/{load.py → _load.py} +1 -1
  1027. scitex/vis/io/_panel.py +432 -0
  1028. scitex/vis/io/{save.py → _save.py} +0 -0
  1029. scitex/vis/model/__init__.py +7 -7
  1030. scitex/vis/model/{annotations.py → _annotations.py} +2 -4
  1031. scitex/vis/model/{axes.py → _axes.py} +1 -1
  1032. scitex/vis/model/{figure.py → _figure.py} +0 -0
  1033. scitex/vis/model/{guides.py → _guides.py} +1 -1
  1034. scitex/vis/model/{plot.py → _plot.py} +2 -4
  1035. scitex/vis/model/{plot_types.py → _plot_types.py} +0 -0
  1036. scitex/vis/model/{styles.py → _styles.py} +0 -0
  1037. scitex/vis/utils/__init__.py +2 -2
  1038. scitex/vis/utils/{defaults.py → _defaults.py} +1 -2
  1039. scitex/vis/utils/{validate.py → _validate.py} +3 -9
  1040. scitex/web/__init__.py +7 -1
  1041. scitex/web/_scraping.py +54 -38
  1042. scitex/web/_search_pubmed.py +30 -14
  1043. scitex/writer/.legacy/Writer_v01-refactored.py +4 -4
  1044. scitex/writer/.legacy/_compile.py +18 -28
  1045. scitex/writer/Writer.py +8 -21
  1046. scitex/writer/__init__.py +11 -11
  1047. scitex/writer/_clone_writer_project.py +2 -6
  1048. scitex/writer/_compile/__init__.py +1 -0
  1049. scitex/writer/_compile/_parser.py +1 -0
  1050. scitex/writer/_compile/_runner.py +35 -38
  1051. scitex/writer/_compile/_validator.py +1 -0
  1052. scitex/writer/_compile/manuscript.py +1 -0
  1053. scitex/writer/_compile/revision.py +1 -0
  1054. scitex/writer/_compile/supplementary.py +1 -0
  1055. scitex/writer/_compile_async.py +5 -12
  1056. scitex/writer/_project/__init__.py +1 -0
  1057. scitex/writer/_project/_create.py +10 -25
  1058. scitex/writer/_project/_trees.py +4 -9
  1059. scitex/writer/_project/_validate.py +2 -3
  1060. scitex/writer/_validate_tree_structures.py +7 -18
  1061. scitex/writer/dataclasses/__init__.py +8 -10
  1062. scitex/writer/dataclasses/config/_CONSTANTS.py +2 -3
  1063. scitex/writer/dataclasses/config/_WriterConfig.py +4 -9
  1064. scitex/writer/dataclasses/contents/_ManuscriptContents.py +14 -25
  1065. scitex/writer/dataclasses/contents/_RevisionContents.py +21 -16
  1066. scitex/writer/dataclasses/contents/_SupplementaryContents.py +21 -24
  1067. scitex/writer/dataclasses/core/_Document.py +2 -3
  1068. scitex/writer/dataclasses/core/_DocumentSection.py +8 -23
  1069. scitex/writer/dataclasses/results/_CompilationResult.py +2 -3
  1070. scitex/writer/dataclasses/results/_LaTeXIssue.py +3 -6
  1071. scitex/writer/dataclasses/results/_SaveSectionsResponse.py +20 -9
  1072. scitex/writer/dataclasses/results/_SectionReadResponse.py +24 -10
  1073. scitex/writer/dataclasses/tree/_ConfigTree.py +7 -4
  1074. scitex/writer/dataclasses/tree/_ManuscriptTree.py +10 -13
  1075. scitex/writer/dataclasses/tree/_RevisionTree.py +16 -17
  1076. scitex/writer/dataclasses/tree/_ScriptsTree.py +10 -5
  1077. scitex/writer/dataclasses/tree/_SharedTree.py +10 -13
  1078. scitex/writer/dataclasses/tree/_SupplementaryTree.py +15 -14
  1079. scitex/writer/utils/.legacy_git_retry.py +3 -8
  1080. scitex/writer/utils/_parse_latex_logs.py +2 -3
  1081. scitex/writer/utils/_parse_script_args.py +20 -23
  1082. scitex/writer/utils/_watch.py +5 -5
  1083. {scitex-2.4.3.dist-info → scitex-2.7.0.dist-info}/METADATA +12 -11
  1084. {scitex-2.4.3.dist-info → scitex-2.7.0.dist-info}/RECORD +1075 -958
  1085. scitex/db/_sqlite3/_SQLite3Mixins/_ColumnMixin_v01-indentation-issues.py +0 -583
  1086. scitex/plt/_subplots/_export_as_csv_formatters.py +0 -112
  1087. scitex/vis/DJANGO_INTEGRATION.md +0 -677
  1088. scitex/vis/editor/_web_editor.py +0 -1440
  1089. scitex/vis/tmp.txt +0 -239
  1090. {scitex-2.4.3.dist-info → scitex-2.7.0.dist-info}/WHEEL +0 -0
  1091. {scitex-2.4.3.dist-info → scitex-2.7.0.dist-info}/entry_points.txt +0 -0
  1092. {scitex-2.4.3.dist-info → scitex-2.7.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,1976 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # Timestamp: 2025-12-08
4
+ # File: ./src/scitex/vis/editor/_dearpygui_editor.py
5
+ """DearPyGui-based figure editor with GPU-accelerated rendering."""
6
+
7
+ from pathlib import Path
8
+ from typing import Dict, Any, Optional
9
+ import copy
10
+ import io
11
+ import base64
12
+
13
+
14
+ def _create_checkerboard(width: int, height: int, square_size: int = 10) -> "Image":
15
+ """Create a checkerboard pattern image for transparency preview.
16
+
17
+ Parameters
18
+ ----------
19
+ width : int
20
+ Image width in pixels
21
+ height : int
22
+ Image height in pixels
23
+ square_size : int
24
+ Size of each checkerboard square (default: 10)
25
+
26
+ Returns
27
+ -------
28
+ PIL.Image
29
+ RGBA image with checkerboard pattern (light/dark gray)
30
+ """
31
+ from PIL import Image
32
+ import numpy as np
33
+
34
+ # Create checkerboard pattern
35
+ light_gray = (220, 220, 220, 255)
36
+ dark_gray = (180, 180, 180, 255)
37
+
38
+ # Create array
39
+ img_array = np.zeros((height, width, 4), dtype=np.uint8)
40
+
41
+ for y in range(height):
42
+ for x in range(width):
43
+ # Determine which square we're in
44
+ square_x = x // square_size
45
+ square_y = y // square_size
46
+ if (square_x + square_y) % 2 == 0:
47
+ img_array[y, x] = light_gray
48
+ else:
49
+ img_array[y, x] = dark_gray
50
+
51
+ return Image.fromarray(img_array, "RGBA")
52
+
53
+
54
+ class DearPyGuiEditor:
55
+ """
56
+ GPU-accelerated figure editor using DearPyGui.
57
+
58
+ Features:
59
+ - Modern immediate-mode GUI with GPU acceleration
60
+ - Real-time figure preview
61
+ - Property editors with sliders, color pickers, and input fields
62
+ - Click-to-select traces on preview
63
+ - Save to .manual.json
64
+ - SciTeX style defaults pre-filled
65
+ - Dark/light theme support
66
+ """
67
+
68
+ def __init__(
69
+ self,
70
+ json_path: Path,
71
+ metadata: Dict[str, Any],
72
+ csv_data: Optional[Any] = None,
73
+ png_path: Optional[Path] = None,
74
+ manual_overrides: Optional[Dict[str, Any]] = None,
75
+ ):
76
+ self.json_path = Path(json_path)
77
+ self.metadata = metadata
78
+ self.csv_data = csv_data
79
+ self.png_path = Path(png_path) if png_path else None
80
+ self.manual_overrides = manual_overrides or {}
81
+
82
+ # Get SciTeX defaults and merge with metadata
83
+ from ._defaults import get_scitex_defaults, extract_defaults_from_metadata
84
+
85
+ self.scitex_defaults = get_scitex_defaults()
86
+ self.metadata_defaults = extract_defaults_from_metadata(metadata)
87
+
88
+ # Start with defaults, then overlay manual overrides
89
+ self.current_overrides = copy.deepcopy(self.scitex_defaults)
90
+ self.current_overrides.update(self.metadata_defaults)
91
+ self.current_overrides.update(self.manual_overrides)
92
+
93
+ # Track modifications
94
+ self._initial_overrides = copy.deepcopy(self.current_overrides)
95
+ self._user_modified = False
96
+ self._texture_id = None
97
+
98
+ # Click-to-select state
99
+ self._selected_element = None # {'type': 'trace'|'title'|'xlabel'|'ylabel'|'legend'|'xaxis'|'yaxis', 'index': int|None}
100
+ self._selected_trace_index = None # Legacy compat
101
+ self._preview_bounds = (
102
+ None # (x_offset, y_offset, width, height) of figure in preview
103
+ )
104
+ self._axes_transform = None # Transform info for data coordinates
105
+ self._element_bboxes = {} # Store bboxes for all selectable elements
106
+
107
+ # Hover state
108
+ self._hovered_element = None # Element currently being hovered
109
+ self._last_hover_check = 0 # For throttling hover updates
110
+ self._backend_name = "dearpygui" # Backend name for title
111
+
112
+ # Cached rendering for fast hover response
113
+ self._cached_base_image = None # PIL Image of base figure (no highlights)
114
+ self._cached_base_data = None # Flattened RGBA data for DearPyGui
115
+ self._cache_dirty = True # Flag to indicate cache needs rebuild
116
+
117
+ def run(self):
118
+ """Launch the DearPyGui editor."""
119
+ try:
120
+ import dearpygui.dearpygui as dpg
121
+ except ImportError:
122
+ raise ImportError(
123
+ "DearPyGui is required for this editor. "
124
+ "Install with: pip install dearpygui"
125
+ )
126
+
127
+ dpg.create_context()
128
+
129
+ # Configure viewport
130
+ dpg.create_viewport(
131
+ title=f"SciTeX Editor ({self._backend_name}) - {self.json_path.name}",
132
+ width=1400,
133
+ height=900,
134
+ )
135
+
136
+ # Create texture registry for image preview
137
+ with dpg.texture_registry(show=False):
138
+ # Create initial texture with placeholder
139
+ width, height = 800, 600
140
+ texture_data = [0.2, 0.2, 0.2, 1.0] * (width * height)
141
+ self._texture_id = dpg.add_dynamic_texture(
142
+ width=width,
143
+ height=height,
144
+ default_value=texture_data,
145
+ tag="preview_texture",
146
+ )
147
+
148
+ # Create main window
149
+ with dpg.window(label="SciTeX Figure Editor", tag="main_window"):
150
+ with dpg.group(horizontal=True):
151
+ # Left panel: Preview
152
+ self._create_preview_panel(dpg)
153
+
154
+ # Right panel: Controls
155
+ self._create_control_panel(dpg)
156
+
157
+ # Set main window as primary
158
+ dpg.set_primary_window("main_window", True)
159
+
160
+ # Initial render
161
+ self._update_preview(dpg)
162
+
163
+ # Setup and show
164
+ dpg.setup_dearpygui()
165
+ dpg.show_viewport()
166
+ dpg.start_dearpygui()
167
+ dpg.destroy_context()
168
+
169
+ def _create_preview_panel(self, dpg):
170
+ """Create the preview panel with figure image, click handler, and hover detection."""
171
+ with dpg.child_window(width=900, height=-1, tag="preview_panel"):
172
+ dpg.add_text(
173
+ "Figure Preview (click to select, hover to highlight)",
174
+ color=(100, 200, 100),
175
+ )
176
+ dpg.add_separator()
177
+
178
+ # Image display with click and move handlers
179
+ with dpg.handler_registry(tag="preview_handler"):
180
+ dpg.add_mouse_click_handler(callback=self._on_preview_click)
181
+ dpg.add_mouse_move_handler(callback=self._on_preview_hover)
182
+
183
+ dpg.add_image("preview_texture", tag="preview_image")
184
+
185
+ dpg.add_separator()
186
+ dpg.add_text("", tag="hover_text", color=(150, 200, 150))
187
+ dpg.add_text("", tag="status_text", color=(150, 150, 150))
188
+ dpg.add_text("", tag="selection_text", color=(200, 200, 100))
189
+
190
+ def _create_control_panel(self, dpg):
191
+ """Create the control panel with all editing options."""
192
+ with dpg.child_window(width=-1, height=-1, tag="control_panel"):
193
+ dpg.add_text("Properties", color=(100, 200, 100))
194
+ dpg.add_separator()
195
+
196
+ # Labels Section
197
+ with dpg.collapsing_header(label="Labels", default_open=True):
198
+ dpg.add_input_text(
199
+ label="Title",
200
+ default_value=self.current_overrides.get("title", ""),
201
+ tag="title_input",
202
+ callback=self._on_value_change,
203
+ on_enter=True,
204
+ width=250,
205
+ )
206
+ dpg.add_input_text(
207
+ label="X Label",
208
+ default_value=self.current_overrides.get("xlabel", ""),
209
+ tag="xlabel_input",
210
+ callback=self._on_value_change,
211
+ on_enter=True,
212
+ width=250,
213
+ )
214
+ dpg.add_input_text(
215
+ label="Y Label",
216
+ default_value=self.current_overrides.get("ylabel", ""),
217
+ tag="ylabel_input",
218
+ callback=self._on_value_change,
219
+ on_enter=True,
220
+ width=250,
221
+ )
222
+
223
+ # Axis Limits Section
224
+ with dpg.collapsing_header(label="Axis Limits", default_open=False):
225
+ with dpg.group(horizontal=True):
226
+ xlim = self.current_overrides.get("xlim", [0, 1])
227
+ dpg.add_input_float(
228
+ label="X Min",
229
+ default_value=xlim[0] if xlim else 0,
230
+ tag="xmin_input",
231
+ width=100,
232
+ )
233
+ dpg.add_input_float(
234
+ label="X Max",
235
+ default_value=xlim[1] if xlim else 1,
236
+ tag="xmax_input",
237
+ width=100,
238
+ )
239
+ with dpg.group(horizontal=True):
240
+ ylim = self.current_overrides.get("ylim", [0, 1])
241
+ dpg.add_input_float(
242
+ label="Y Min",
243
+ default_value=ylim[0] if ylim else 0,
244
+ tag="ymin_input",
245
+ width=100,
246
+ )
247
+ dpg.add_input_float(
248
+ label="Y Max",
249
+ default_value=ylim[1] if ylim else 1,
250
+ tag="ymax_input",
251
+ width=100,
252
+ )
253
+ dpg.add_button(
254
+ label="Apply Limits",
255
+ callback=self._apply_limits,
256
+ width=150,
257
+ )
258
+
259
+ # Line Style Section
260
+ with dpg.collapsing_header(label="Line Style", default_open=True):
261
+ dpg.add_slider_float(
262
+ label="Line Width (pt)",
263
+ default_value=self.current_overrides.get("linewidth", 1.0),
264
+ min_value=0.1,
265
+ max_value=5.0,
266
+ tag="linewidth_slider",
267
+ callback=self._on_value_change,
268
+ width=200,
269
+ )
270
+
271
+ # Font Settings Section
272
+ with dpg.collapsing_header(label="Font Settings", default_open=False):
273
+ dpg.add_slider_int(
274
+ label="Title Font Size",
275
+ default_value=self.current_overrides.get("title_fontsize", 8),
276
+ min_value=6,
277
+ max_value=20,
278
+ tag="title_fontsize_slider",
279
+ callback=self._on_value_change,
280
+ width=200,
281
+ )
282
+ dpg.add_slider_int(
283
+ label="Axis Font Size",
284
+ default_value=self.current_overrides.get("axis_fontsize", 7),
285
+ min_value=6,
286
+ max_value=16,
287
+ tag="axis_fontsize_slider",
288
+ callback=self._on_value_change,
289
+ width=200,
290
+ )
291
+ dpg.add_slider_int(
292
+ label="Tick Font Size",
293
+ default_value=self.current_overrides.get("tick_fontsize", 7),
294
+ min_value=6,
295
+ max_value=16,
296
+ tag="tick_fontsize_slider",
297
+ callback=self._on_value_change,
298
+ width=200,
299
+ )
300
+ dpg.add_slider_int(
301
+ label="Legend Font Size",
302
+ default_value=self.current_overrides.get("legend_fontsize", 6),
303
+ min_value=4,
304
+ max_value=14,
305
+ tag="legend_fontsize_slider",
306
+ callback=self._on_value_change,
307
+ width=200,
308
+ )
309
+
310
+ # Tick Settings Section
311
+ with dpg.collapsing_header(label="Tick Settings", default_open=False):
312
+ dpg.add_slider_int(
313
+ label="N Ticks",
314
+ default_value=self.current_overrides.get("n_ticks", 4),
315
+ min_value=2,
316
+ max_value=10,
317
+ tag="n_ticks_slider",
318
+ callback=self._on_value_change,
319
+ width=200,
320
+ )
321
+ dpg.add_slider_float(
322
+ label="Tick Length (mm)",
323
+ default_value=self.current_overrides.get("tick_length", 0.8),
324
+ min_value=0.2,
325
+ max_value=3.0,
326
+ tag="tick_length_slider",
327
+ callback=self._on_value_change,
328
+ width=200,
329
+ )
330
+ dpg.add_slider_float(
331
+ label="Tick Width (mm)",
332
+ default_value=self.current_overrides.get("tick_width", 0.2),
333
+ min_value=0.05,
334
+ max_value=1.0,
335
+ tag="tick_width_slider",
336
+ callback=self._on_value_change,
337
+ width=200,
338
+ )
339
+ dpg.add_combo(
340
+ label="Tick Direction",
341
+ items=["out", "in", "inout"],
342
+ default_value=self.current_overrides.get("tick_direction", "out"),
343
+ tag="tick_direction_combo",
344
+ callback=self._on_value_change,
345
+ width=150,
346
+ )
347
+
348
+ # Style Section
349
+ with dpg.collapsing_header(label="Style", default_open=True):
350
+ dpg.add_checkbox(
351
+ label="Show Grid",
352
+ default_value=self.current_overrides.get("grid", False),
353
+ tag="grid_checkbox",
354
+ callback=self._on_value_change,
355
+ )
356
+ dpg.add_checkbox(
357
+ label="Hide Top Spine",
358
+ default_value=self.current_overrides.get("hide_top_spine", True),
359
+ tag="hide_top_spine_checkbox",
360
+ callback=self._on_value_change,
361
+ )
362
+ dpg.add_checkbox(
363
+ label="Hide Right Spine",
364
+ default_value=self.current_overrides.get("hide_right_spine", True),
365
+ tag="hide_right_spine_checkbox",
366
+ callback=self._on_value_change,
367
+ )
368
+ dpg.add_checkbox(
369
+ label="Transparent Background",
370
+ default_value=self.current_overrides.get("transparent", True),
371
+ tag="transparent_checkbox",
372
+ callback=self._on_value_change,
373
+ )
374
+ dpg.add_slider_float(
375
+ label="Axis Width (mm)",
376
+ default_value=self.current_overrides.get("axis_width", 0.2),
377
+ min_value=0.05,
378
+ max_value=1.0,
379
+ tag="axis_width_slider",
380
+ callback=self._on_value_change,
381
+ width=200,
382
+ )
383
+
384
+ # Legend Section
385
+ with dpg.collapsing_header(label="Legend", default_open=False):
386
+ dpg.add_checkbox(
387
+ label="Show Legend",
388
+ default_value=self.current_overrides.get("legend_visible", True),
389
+ tag="legend_visible_checkbox",
390
+ callback=self._on_value_change,
391
+ )
392
+ dpg.add_checkbox(
393
+ label="Show Frame",
394
+ default_value=self.current_overrides.get("legend_frameon", False),
395
+ tag="legend_frameon_checkbox",
396
+ callback=self._on_value_change,
397
+ )
398
+ dpg.add_combo(
399
+ label="Position",
400
+ items=[
401
+ "best",
402
+ "upper right",
403
+ "upper left",
404
+ "lower right",
405
+ "lower left",
406
+ "center right",
407
+ "center left",
408
+ "upper center",
409
+ "lower center",
410
+ "center",
411
+ ],
412
+ default_value=self.current_overrides.get("legend_loc", "best"),
413
+ tag="legend_loc_combo",
414
+ callback=self._on_value_change,
415
+ width=150,
416
+ )
417
+
418
+ # Selected Element Section (click on preview to select)
419
+ with dpg.collapsing_header(
420
+ label="Selected Element",
421
+ default_open=True,
422
+ tag="selected_element_header",
423
+ ):
424
+ dpg.add_text(
425
+ "Click on preview to select elements",
426
+ tag="element_hint_text",
427
+ color=(150, 150, 150),
428
+ )
429
+ dpg.add_combo(
430
+ label="Element",
431
+ items=self._get_all_element_labels(),
432
+ tag="element_selector_combo",
433
+ callback=self._on_element_selected,
434
+ width=200,
435
+ )
436
+ dpg.add_separator()
437
+
438
+ # Trace-specific controls (shown when trace selected)
439
+ with dpg.group(tag="trace_controls_group", show=False):
440
+ dpg.add_input_text(
441
+ label="Label",
442
+ tag="trace_label_input",
443
+ callback=self._on_trace_property_change,
444
+ on_enter=True,
445
+ width=200,
446
+ )
447
+ dpg.add_color_edit(
448
+ label="Color",
449
+ tag="trace_color_picker",
450
+ callback=self._on_trace_property_change,
451
+ no_alpha=True,
452
+ width=200,
453
+ )
454
+ dpg.add_slider_float(
455
+ label="Line Width",
456
+ tag="trace_linewidth_slider",
457
+ default_value=1.0,
458
+ min_value=0.1,
459
+ max_value=5.0,
460
+ callback=self._on_trace_property_change,
461
+ width=200,
462
+ )
463
+ dpg.add_combo(
464
+ label="Line Style",
465
+ items=["-", "--", "-.", ":", ""],
466
+ default_value="-",
467
+ tag="trace_linestyle_combo",
468
+ callback=self._on_trace_property_change,
469
+ width=100,
470
+ )
471
+ dpg.add_combo(
472
+ label="Marker",
473
+ items=["", "o", "s", "^", "v", "D", "x", "+", "*"],
474
+ default_value="",
475
+ tag="trace_marker_combo",
476
+ callback=self._on_trace_property_change,
477
+ width=100,
478
+ )
479
+ dpg.add_slider_float(
480
+ label="Marker Size",
481
+ tag="trace_markersize_slider",
482
+ default_value=6.0,
483
+ min_value=1.0,
484
+ max_value=20.0,
485
+ callback=self._on_trace_property_change,
486
+ width=200,
487
+ )
488
+
489
+ # Text element controls (title, xlabel, ylabel)
490
+ with dpg.group(tag="text_controls_group", show=False):
491
+ dpg.add_input_text(
492
+ label="Text",
493
+ tag="element_text_input",
494
+ callback=self._on_text_element_change,
495
+ on_enter=True,
496
+ width=200,
497
+ )
498
+ dpg.add_slider_int(
499
+ label="Font Size",
500
+ tag="element_fontsize_slider",
501
+ default_value=8,
502
+ min_value=4,
503
+ max_value=24,
504
+ callback=self._on_text_element_change,
505
+ width=200,
506
+ )
507
+ dpg.add_color_edit(
508
+ label="Color",
509
+ tag="element_text_color",
510
+ callback=self._on_text_element_change,
511
+ no_alpha=True,
512
+ default_value=[0, 0, 0],
513
+ width=200,
514
+ )
515
+
516
+ # Axis element controls (xaxis, yaxis)
517
+ with dpg.group(tag="axis_controls_group", show=False):
518
+ dpg.add_slider_float(
519
+ label="Line Width (mm)",
520
+ tag="axis_linewidth_slider",
521
+ default_value=0.2,
522
+ min_value=0.05,
523
+ max_value=1.0,
524
+ callback=self._on_axis_element_change,
525
+ width=200,
526
+ )
527
+ dpg.add_slider_float(
528
+ label="Tick Length (mm)",
529
+ tag="axis_tick_length_slider",
530
+ default_value=0.8,
531
+ min_value=0.2,
532
+ max_value=3.0,
533
+ callback=self._on_axis_element_change,
534
+ width=200,
535
+ )
536
+ dpg.add_slider_int(
537
+ label="Tick Font Size",
538
+ tag="axis_tick_fontsize_slider",
539
+ default_value=7,
540
+ min_value=4,
541
+ max_value=16,
542
+ callback=self._on_axis_element_change,
543
+ width=200,
544
+ )
545
+ dpg.add_checkbox(
546
+ label="Show Spine",
547
+ tag="axis_show_spine_checkbox",
548
+ default_value=True,
549
+ callback=self._on_axis_element_change,
550
+ )
551
+
552
+ # Legend controls
553
+ with dpg.group(tag="legend_controls_group", show=False):
554
+ dpg.add_checkbox(
555
+ label="Visible",
556
+ tag="legend_visible_edit",
557
+ default_value=True,
558
+ callback=self._on_legend_element_change,
559
+ )
560
+ dpg.add_checkbox(
561
+ label="Show Frame",
562
+ tag="legend_frameon_edit",
563
+ default_value=False,
564
+ callback=self._on_legend_element_change,
565
+ )
566
+ dpg.add_combo(
567
+ label="Position",
568
+ items=[
569
+ "best",
570
+ "upper right",
571
+ "upper left",
572
+ "lower right",
573
+ "lower left",
574
+ "center right",
575
+ "center left",
576
+ "upper center",
577
+ "lower center",
578
+ "center",
579
+ ],
580
+ default_value="best",
581
+ tag="legend_loc_edit",
582
+ callback=self._on_legend_element_change,
583
+ width=150,
584
+ )
585
+ dpg.add_slider_int(
586
+ label="Font Size",
587
+ tag="legend_fontsize_edit",
588
+ default_value=6,
589
+ min_value=4,
590
+ max_value=14,
591
+ callback=self._on_legend_element_change,
592
+ width=200,
593
+ )
594
+
595
+ dpg.add_button(
596
+ label="Deselect",
597
+ callback=self._deselect_element,
598
+ width=100,
599
+ )
600
+
601
+ # Dimensions Section
602
+ with dpg.collapsing_header(label="Dimensions", default_open=False):
603
+ fig_size = self.current_overrides.get("fig_size", [3.15, 2.68])
604
+ with dpg.group(horizontal=True):
605
+ dpg.add_input_float(
606
+ label="Width (in)",
607
+ default_value=fig_size[0],
608
+ tag="fig_width_input",
609
+ width=100,
610
+ )
611
+ dpg.add_input_float(
612
+ label="Height (in)",
613
+ default_value=fig_size[1],
614
+ tag="fig_height_input",
615
+ width=100,
616
+ )
617
+ dpg.add_slider_int(
618
+ label="DPI",
619
+ default_value=self.current_overrides.get("dpi", 300),
620
+ min_value=72,
621
+ max_value=600,
622
+ tag="dpi_slider",
623
+ callback=self._on_value_change,
624
+ width=200,
625
+ )
626
+
627
+ # Annotations Section
628
+ with dpg.collapsing_header(label="Annotations", default_open=False):
629
+ dpg.add_input_text(
630
+ label="Text",
631
+ tag="annot_text_input",
632
+ width=200,
633
+ )
634
+ with dpg.group(horizontal=True):
635
+ dpg.add_input_float(
636
+ label="X",
637
+ default_value=0.5,
638
+ tag="annot_x_input",
639
+ width=80,
640
+ )
641
+ dpg.add_input_float(
642
+ label="Y",
643
+ default_value=0.5,
644
+ tag="annot_y_input",
645
+ width=80,
646
+ )
647
+ dpg.add_button(
648
+ label="Add Annotation",
649
+ callback=self._add_annotation,
650
+ width=150,
651
+ )
652
+ dpg.add_listbox(
653
+ label="",
654
+ items=[],
655
+ tag="annotations_listbox",
656
+ num_items=4,
657
+ width=250,
658
+ )
659
+ dpg.add_button(
660
+ label="Remove Selected",
661
+ callback=self._remove_annotation,
662
+ width=150,
663
+ )
664
+
665
+ dpg.add_separator()
666
+
667
+ # Action buttons
668
+ dpg.add_button(
669
+ label="Update Preview",
670
+ callback=lambda: self._update_preview(dpg),
671
+ width=-1,
672
+ )
673
+ dpg.add_button(
674
+ label="Save to .manual.json",
675
+ callback=self._save_manual,
676
+ width=-1,
677
+ )
678
+ dpg.add_button(
679
+ label="Reset to Original",
680
+ callback=self._reset_overrides,
681
+ width=-1,
682
+ )
683
+ dpg.add_button(
684
+ label="Export PNG",
685
+ callback=self._export_png,
686
+ width=-1,
687
+ )
688
+
689
+ def _on_value_change(self, sender, app_data, user_data=None):
690
+ """Handle value changes from widgets."""
691
+ import dearpygui.dearpygui as dpg
692
+
693
+ self._user_modified = True
694
+ self._collect_overrides(dpg)
695
+ self._update_preview(dpg)
696
+
697
+ def _collect_overrides(self, dpg):
698
+ """Collect current values from all widgets."""
699
+ # Labels
700
+ self.current_overrides["title"] = dpg.get_value("title_input")
701
+ self.current_overrides["xlabel"] = dpg.get_value("xlabel_input")
702
+ self.current_overrides["ylabel"] = dpg.get_value("ylabel_input")
703
+
704
+ # Line style
705
+ self.current_overrides["linewidth"] = dpg.get_value("linewidth_slider")
706
+
707
+ # Font settings
708
+ self.current_overrides["title_fontsize"] = dpg.get_value(
709
+ "title_fontsize_slider"
710
+ )
711
+ self.current_overrides["axis_fontsize"] = dpg.get_value("axis_fontsize_slider")
712
+ self.current_overrides["tick_fontsize"] = dpg.get_value("tick_fontsize_slider")
713
+ self.current_overrides["legend_fontsize"] = dpg.get_value(
714
+ "legend_fontsize_slider"
715
+ )
716
+
717
+ # Tick settings
718
+ self.current_overrides["n_ticks"] = dpg.get_value("n_ticks_slider")
719
+ self.current_overrides["tick_length"] = dpg.get_value("tick_length_slider")
720
+ self.current_overrides["tick_width"] = dpg.get_value("tick_width_slider")
721
+ self.current_overrides["tick_direction"] = dpg.get_value("tick_direction_combo")
722
+
723
+ # Style
724
+ self.current_overrides["grid"] = dpg.get_value("grid_checkbox")
725
+ self.current_overrides["hide_top_spine"] = dpg.get_value(
726
+ "hide_top_spine_checkbox"
727
+ )
728
+ self.current_overrides["hide_right_spine"] = dpg.get_value(
729
+ "hide_right_spine_checkbox"
730
+ )
731
+ self.current_overrides["transparent"] = dpg.get_value("transparent_checkbox")
732
+ self.current_overrides["axis_width"] = dpg.get_value("axis_width_slider")
733
+
734
+ # Legend
735
+ self.current_overrides["legend_visible"] = dpg.get_value(
736
+ "legend_visible_checkbox"
737
+ )
738
+ self.current_overrides["legend_frameon"] = dpg.get_value(
739
+ "legend_frameon_checkbox"
740
+ )
741
+ self.current_overrides["legend_loc"] = dpg.get_value("legend_loc_combo")
742
+
743
+ # Dimensions
744
+ self.current_overrides["fig_size"] = [
745
+ dpg.get_value("fig_width_input"),
746
+ dpg.get_value("fig_height_input"),
747
+ ]
748
+ self.current_overrides["dpi"] = dpg.get_value("dpi_slider")
749
+
750
+ def _apply_limits(self, sender=None, app_data=None, user_data=None):
751
+ """Apply axis limits."""
752
+ import dearpygui.dearpygui as dpg
753
+
754
+ xmin = dpg.get_value("xmin_input")
755
+ xmax = dpg.get_value("xmax_input")
756
+ ymin = dpg.get_value("ymin_input")
757
+ ymax = dpg.get_value("ymax_input")
758
+
759
+ if xmin < xmax:
760
+ self.current_overrides["xlim"] = [xmin, xmax]
761
+ if ymin < ymax:
762
+ self.current_overrides["ylim"] = [ymin, ymax]
763
+
764
+ self._user_modified = True
765
+ self._update_preview(dpg)
766
+
767
+ def _add_annotation(self, sender=None, app_data=None, user_data=None):
768
+ """Add text annotation."""
769
+ import dearpygui.dearpygui as dpg
770
+
771
+ text = dpg.get_value("annot_text_input")
772
+ if not text:
773
+ return
774
+
775
+ x = dpg.get_value("annot_x_input")
776
+ y = dpg.get_value("annot_y_input")
777
+
778
+ if "annotations" not in self.current_overrides:
779
+ self.current_overrides["annotations"] = []
780
+
781
+ self.current_overrides["annotations"].append(
782
+ {
783
+ "type": "text",
784
+ "text": text,
785
+ "x": x,
786
+ "y": y,
787
+ "fontsize": self.current_overrides.get("axis_fontsize", 7),
788
+ }
789
+ )
790
+
791
+ dpg.set_value("annot_text_input", "")
792
+ self._update_annotations_list(dpg)
793
+ self._user_modified = True
794
+ self._update_preview(dpg)
795
+
796
+ def _remove_annotation(self, sender=None, app_data=None, user_data=None):
797
+ """Remove selected annotation."""
798
+ import dearpygui.dearpygui as dpg
799
+
800
+ selected = dpg.get_value("annotations_listbox")
801
+ annotations = self.current_overrides.get("annotations", [])
802
+
803
+ if selected and annotations:
804
+ # Find index by text
805
+ for i, ann in enumerate(annotations):
806
+ label = f"{ann.get('text', '')[:20]} ({ann.get('x', 0):.2f}, {ann.get('y', 0):.2f})"
807
+ if label == selected:
808
+ del annotations[i]
809
+ break
810
+
811
+ self._update_annotations_list(dpg)
812
+ self._user_modified = True
813
+ self._update_preview(dpg)
814
+
815
+ def _update_annotations_list(self, dpg):
816
+ """Update the annotations listbox."""
817
+ annotations = self.current_overrides.get("annotations", [])
818
+ items = []
819
+ for ann in annotations:
820
+ if ann.get("type") == "text":
821
+ label = f"{ann.get('text', '')[:20]} ({ann.get('x', 0):.2f}, {ann.get('y', 0):.2f})"
822
+ items.append(label)
823
+
824
+ dpg.configure_item("annotations_listbox", items=items)
825
+
826
+ def _update_preview(self, dpg):
827
+ """Update the figure preview (full re-render)."""
828
+ try:
829
+ # Mark cache dirty and do full render
830
+ self._cache_dirty = True
831
+ img_data, width, height = self._render_figure()
832
+
833
+ # Update texture
834
+ dpg.set_value("preview_texture", img_data)
835
+
836
+ # Update status
837
+ dpg.set_value("status_text", f"Preview updated ({width}x{height})")
838
+
839
+ except Exception as e:
840
+ dpg.set_value("status_text", f"Error: {str(e)}")
841
+
842
+ def _update_hover_overlay(self, dpg):
843
+ """Fast hover overlay update using cached base image (no matplotlib re-render)."""
844
+ import numpy as np
845
+ from PIL import Image, ImageDraw
846
+
847
+ # If no cached base, do full render
848
+ if self._cached_base_image is None:
849
+ self._update_preview(dpg)
850
+ return
851
+
852
+ try:
853
+ # Start with a copy of cached base
854
+ img = self._cached_base_image.copy()
855
+ draw = ImageDraw.Draw(img, "RGBA")
856
+
857
+ # Get hover element type
858
+ hovered_type = (
859
+ self._hovered_element.get("type") if self._hovered_element else None
860
+ )
861
+ selected_type = (
862
+ self._selected_element.get("type") if self._selected_element else None
863
+ )
864
+
865
+ # Draw hover highlight (outline only, no fill) for non-trace elements
866
+ if (
867
+ hovered_type
868
+ and hovered_type != "trace"
869
+ and hovered_type != selected_type
870
+ ):
871
+ bbox = self._element_bboxes.get(hovered_type)
872
+ if bbox:
873
+ x0, y0, x1, y1 = bbox
874
+ # Transparent outline only - no fill to avoid covering content
875
+ draw.rectangle(
876
+ [x0 - 2, y0 - 2, x1 + 2, y1 + 2],
877
+ fill=None,
878
+ outline=(100, 180, 255, 100),
879
+ width=1,
880
+ )
881
+
882
+ # Draw selection highlight (outline only, no fill) for non-trace elements
883
+ if selected_type and selected_type != "trace":
884
+ bbox = self._element_bboxes.get(selected_type)
885
+ if bbox:
886
+ x0, y0, x1, y1 = bbox
887
+ # Transparent outline only - no fill to avoid covering content
888
+ draw.rectangle(
889
+ [x0 - 2, y0 - 2, x1 + 2, y1 + 2],
890
+ fill=None,
891
+ outline=(255, 200, 80, 150),
892
+ width=2,
893
+ )
894
+
895
+ # Convert to DearPyGui texture format
896
+ img_array = np.array(img).astype(np.float32) / 255.0
897
+ img_data = img_array.flatten().tolist()
898
+
899
+ # Update texture
900
+ dpg.set_value("preview_texture", img_data)
901
+
902
+ except Exception as e:
903
+ # Fallback to full render on error
904
+ self._update_preview(dpg)
905
+
906
+ def _render_figure(self):
907
+ """Render figure and return as RGBA data for texture."""
908
+ import matplotlib
909
+
910
+ matplotlib.use("Agg")
911
+ import matplotlib.pyplot as plt
912
+ from matplotlib.ticker import MaxNLocator
913
+ import numpy as np
914
+ from PIL import Image
915
+ import dearpygui.dearpygui as dpg
916
+
917
+ # mm to pt conversion
918
+ mm_to_pt = 2.83465
919
+
920
+ o = self.current_overrides
921
+
922
+ # Dimensions - use fixed size for preview
923
+ preview_dpi = 100
924
+ fig_size = o.get("fig_size", [3.15, 2.68])
925
+
926
+ # Create figure with white background for preview
927
+ fig, ax = plt.subplots(figsize=fig_size, dpi=preview_dpi)
928
+
929
+ # For preview, use white background (transparent doesn't show well in GUI)
930
+ fig.patch.set_facecolor("white")
931
+ ax.patch.set_facecolor("white")
932
+
933
+ # Plot from CSV data (only pass selection, hover is via PIL overlay for speed)
934
+ if self.csv_data is not None:
935
+ self._plot_from_csv(ax, o, highlight_trace=self._selected_trace_index)
936
+ else:
937
+ ax.text(
938
+ 0.5,
939
+ 0.5,
940
+ "No plot data available\n(CSV not found)",
941
+ ha="center",
942
+ va="center",
943
+ transform=ax.transAxes,
944
+ fontsize=o.get("axis_fontsize", 7),
945
+ )
946
+
947
+ # Apply labels
948
+ if o.get("title"):
949
+ ax.set_title(o["title"], fontsize=o.get("title_fontsize", 8))
950
+ if o.get("xlabel"):
951
+ ax.set_xlabel(o["xlabel"], fontsize=o.get("axis_fontsize", 7))
952
+ if o.get("ylabel"):
953
+ ax.set_ylabel(o["ylabel"], fontsize=o.get("axis_fontsize", 7))
954
+
955
+ # Tick styling
956
+ ax.tick_params(
957
+ axis="both",
958
+ labelsize=o.get("tick_fontsize", 7),
959
+ length=o.get("tick_length", 0.8) * mm_to_pt,
960
+ width=o.get("tick_width", 0.2) * mm_to_pt,
961
+ direction=o.get("tick_direction", "out"),
962
+ )
963
+
964
+ # Number of ticks
965
+ ax.xaxis.set_major_locator(MaxNLocator(nbins=o.get("n_ticks", 4)))
966
+ ax.yaxis.set_major_locator(MaxNLocator(nbins=o.get("n_ticks", 4)))
967
+
968
+ # Grid
969
+ if o.get("grid"):
970
+ ax.grid(True, linewidth=o.get("axis_width", 0.2) * mm_to_pt, alpha=0.3)
971
+
972
+ # Axis limits
973
+ if o.get("xlim"):
974
+ ax.set_xlim(o["xlim"])
975
+ if o.get("ylim"):
976
+ ax.set_ylim(o["ylim"])
977
+
978
+ # Spines
979
+ if o.get("hide_top_spine", True):
980
+ ax.spines["top"].set_visible(False)
981
+ if o.get("hide_right_spine", True):
982
+ ax.spines["right"].set_visible(False)
983
+
984
+ for spine in ax.spines.values():
985
+ spine.set_linewidth(o.get("axis_width", 0.2) * mm_to_pt)
986
+
987
+ # Annotations
988
+ for annot in o.get("annotations", []):
989
+ if annot.get("type") == "text":
990
+ ax.text(
991
+ annot.get("x", 0.5),
992
+ annot.get("y", 0.5),
993
+ annot.get("text", ""),
994
+ transform=ax.transAxes,
995
+ fontsize=annot.get("fontsize", o.get("axis_fontsize", 7)),
996
+ )
997
+
998
+ fig.tight_layout()
999
+
1000
+ # Draw before collecting bboxes so we have accurate positions
1001
+ fig.canvas.draw()
1002
+
1003
+ # Draw hover/selection highlights for non-trace elements
1004
+ self._draw_element_highlights(fig, ax)
1005
+
1006
+ # Store axes transform info for click-to-select
1007
+ fig.canvas.draw()
1008
+ ax_bbox = ax.get_position()
1009
+ fig_width_px = int(fig_size[0] * preview_dpi)
1010
+ fig_height_px = int(fig_size[1] * preview_dpi)
1011
+
1012
+ # Collect element bboxes for click detection (in figure pixel coordinates)
1013
+ # We'll scale these later after resize
1014
+ self._element_bboxes_raw = {}
1015
+
1016
+ # Title bbox
1017
+ if ax.title.get_text():
1018
+ try:
1019
+ title_bbox = ax.title.get_window_extent(fig.canvas.get_renderer())
1020
+ self._element_bboxes_raw["title"] = (
1021
+ title_bbox.x0,
1022
+ title_bbox.y0,
1023
+ title_bbox.x1,
1024
+ title_bbox.y1,
1025
+ )
1026
+ except Exception:
1027
+ pass
1028
+
1029
+ # X label bbox
1030
+ if ax.xaxis.label.get_text():
1031
+ try:
1032
+ xlabel_bbox = ax.xaxis.label.get_window_extent(
1033
+ fig.canvas.get_renderer()
1034
+ )
1035
+ self._element_bboxes_raw["xlabel"] = (
1036
+ xlabel_bbox.x0,
1037
+ xlabel_bbox.y0,
1038
+ xlabel_bbox.x1,
1039
+ xlabel_bbox.y1,
1040
+ )
1041
+ except Exception:
1042
+ pass
1043
+
1044
+ # Y label bbox
1045
+ if ax.yaxis.label.get_text():
1046
+ try:
1047
+ ylabel_bbox = ax.yaxis.label.get_window_extent(
1048
+ fig.canvas.get_renderer()
1049
+ )
1050
+ self._element_bboxes_raw["ylabel"] = (
1051
+ ylabel_bbox.x0,
1052
+ ylabel_bbox.y0,
1053
+ ylabel_bbox.x1,
1054
+ ylabel_bbox.y1,
1055
+ )
1056
+ except Exception:
1057
+ pass
1058
+
1059
+ # Legend bbox
1060
+ legend = ax.get_legend()
1061
+ if legend:
1062
+ try:
1063
+ legend_bbox = legend.get_window_extent(fig.canvas.get_renderer())
1064
+ self._element_bboxes_raw["legend"] = (
1065
+ legend_bbox.x0,
1066
+ legend_bbox.y0,
1067
+ legend_bbox.x1,
1068
+ legend_bbox.y1,
1069
+ )
1070
+ except Exception:
1071
+ pass
1072
+
1073
+ # X axis (bottom spine area)
1074
+ try:
1075
+ xaxis_bbox = ax.spines["bottom"].get_window_extent(
1076
+ fig.canvas.get_renderer()
1077
+ )
1078
+ # Expand bbox slightly for easier clicking
1079
+ self._element_bboxes_raw["xaxis"] = (
1080
+ xaxis_bbox.x0,
1081
+ xaxis_bbox.y0 - 20,
1082
+ xaxis_bbox.x1,
1083
+ xaxis_bbox.y1 + 10,
1084
+ )
1085
+ except Exception:
1086
+ pass
1087
+
1088
+ # Y axis (left spine area)
1089
+ try:
1090
+ yaxis_bbox = ax.spines["left"].get_window_extent(fig.canvas.get_renderer())
1091
+ # Expand bbox slightly for easier clicking
1092
+ self._element_bboxes_raw["yaxis"] = (
1093
+ yaxis_bbox.x0 - 20,
1094
+ yaxis_bbox.y0,
1095
+ yaxis_bbox.x1 + 10,
1096
+ yaxis_bbox.y1,
1097
+ )
1098
+ except Exception:
1099
+ pass
1100
+
1101
+ # Convert to RGBA data for DearPyGui texture
1102
+ buf = io.BytesIO()
1103
+ fig.savefig(
1104
+ buf,
1105
+ format="png",
1106
+ dpi=preview_dpi,
1107
+ bbox_inches="tight",
1108
+ facecolor="white",
1109
+ edgecolor="none",
1110
+ )
1111
+ buf.seek(0)
1112
+
1113
+ # Load with PIL and convert to normalized RGBA
1114
+ img = Image.open(buf).convert("RGBA")
1115
+ width, height = img.size
1116
+
1117
+ # Resize to fit within max preview size while preserving aspect ratio
1118
+ max_width, max_height = 800, 600
1119
+ ratio = min(max_width / width, max_height / height)
1120
+ new_width = int(width * ratio)
1121
+ new_height = int(height * ratio)
1122
+ img = img.resize((new_width, new_height), Image.LANCZOS)
1123
+
1124
+ # Store preview bounds for coordinate conversion (after resize)
1125
+ x_offset = (max_width - new_width) // 2
1126
+ y_offset = (max_height - new_height) // 2
1127
+ self._preview_bounds = (x_offset, y_offset, new_width, new_height)
1128
+
1129
+ # Scale element bboxes to preview coordinates
1130
+ # Note: matplotlib uses bottom-left origin, we need top-left for preview
1131
+ self._element_bboxes = {}
1132
+ for elem_type, raw_bbox in getattr(self, "_element_bboxes_raw", {}).items():
1133
+ if raw_bbox is None:
1134
+ continue
1135
+ rx0, ry0, rx1, ry1 = raw_bbox
1136
+ # Scale to resized image
1137
+ sx0 = int(rx0 * ratio) + x_offset
1138
+ sx1 = int(rx1 * ratio) + x_offset
1139
+ # Flip Y coordinate (matplotlib origin is bottom, preview is top)
1140
+ sy0 = new_height - int(ry1 * ratio) + y_offset
1141
+ sy1 = new_height - int(ry0 * ratio) + y_offset
1142
+ self._element_bboxes[elem_type] = (sx0, sy0, sx1, sy1)
1143
+
1144
+ # Store axes transform info (scaled to resized image)
1145
+ # ax_bbox is in figure fraction coordinates
1146
+ ax_x0 = int(ax_bbox.x0 * new_width)
1147
+ ax_y0 = int((1 - ax_bbox.y1) * new_height) # Flip y (0 at top)
1148
+ ax_width = int(ax_bbox.width * new_width)
1149
+ ax_height = int(ax_bbox.height * new_height)
1150
+ xlim = ax.get_xlim()
1151
+ ylim = ax.get_ylim()
1152
+ self._axes_transform = (ax_x0, ax_y0, ax_width, ax_height, xlim, ylim)
1153
+
1154
+ # Create background - checkerboard for transparent, white otherwise
1155
+ transparent = o.get("transparent", True)
1156
+ if transparent:
1157
+ # Create checkerboard pattern for transparency preview
1158
+ padded = _create_checkerboard(max_width, max_height, square_size=10)
1159
+ else:
1160
+ padded = Image.new("RGBA", (max_width, max_height), (255, 255, 255, 255))
1161
+
1162
+ # Paste figure centered on background
1163
+ padded.paste(img, (x_offset, y_offset), img) # Use img as mask for alpha
1164
+ img = padded
1165
+ width, height = max_width, max_height
1166
+
1167
+ # Cache the base image (without highlights) for fast hover updates
1168
+ self._cached_base_image = img.copy()
1169
+ self._cache_dirty = False
1170
+
1171
+ # Convert to normalized float array for DearPyGui
1172
+ img_array = np.array(img).astype(np.float32) / 255.0
1173
+ img_data = img_array.flatten().tolist()
1174
+
1175
+ plt.close(fig)
1176
+
1177
+ # Update texture data (don't recreate texture, just update values)
1178
+ dpg.set_value("preview_texture", img_data)
1179
+
1180
+ return img_data, width, height
1181
+
1182
+ def _draw_element_highlights(self, fig, ax):
1183
+ """Draw selection highlights for non-trace elements (hover handled via PIL overlay)."""
1184
+ from matplotlib.patches import FancyBboxPatch
1185
+ import matplotlib.transforms as transforms
1186
+
1187
+ renderer = fig.canvas.get_renderer()
1188
+
1189
+ # Only draw selection highlights here (hover is done via fast PIL overlay)
1190
+ selected_type = (
1191
+ self._selected_element.get("type") if self._selected_element else None
1192
+ )
1193
+
1194
+ # Skip if selecting traces (handled separately in _plot_from_csv)
1195
+ if selected_type == "trace":
1196
+ selected_type = None
1197
+
1198
+ def add_highlight_box(text_obj, color, alpha, linewidth=2):
1199
+ """Add highlight rectangle around a text object (outline only)."""
1200
+ try:
1201
+ bbox = text_obj.get_window_extent(renderer)
1202
+ # Convert to figure coordinates
1203
+ fig_bbox = bbox.transformed(fig.transFigure.inverted())
1204
+ # Add padding
1205
+ padding = 0.01
1206
+ rect = FancyBboxPatch(
1207
+ (fig_bbox.x0 - padding, fig_bbox.y0 - padding),
1208
+ fig_bbox.width + 2 * padding,
1209
+ fig_bbox.height + 2 * padding,
1210
+ boxstyle="round,pad=0.02,rounding_size=0.01",
1211
+ facecolor="none",
1212
+ edgecolor=color,
1213
+ alpha=0.7,
1214
+ linewidth=linewidth,
1215
+ transform=fig.transFigure,
1216
+ zorder=100,
1217
+ )
1218
+ fig.patches.append(rect)
1219
+ except Exception:
1220
+ pass
1221
+
1222
+ def add_spine_highlight(spine, color, alpha, linewidth=2):
1223
+ """Add highlight to a spine/axis (outline only)."""
1224
+ try:
1225
+ bbox = spine.get_window_extent(renderer)
1226
+ fig_bbox = bbox.transformed(fig.transFigure.inverted())
1227
+ padding = 0.01
1228
+ rect = FancyBboxPatch(
1229
+ (fig_bbox.x0 - padding, fig_bbox.y0 - padding),
1230
+ fig_bbox.width + 2 * padding,
1231
+ fig_bbox.height + 2 * padding,
1232
+ boxstyle="round,pad=0.01",
1233
+ facecolor="none",
1234
+ edgecolor=color,
1235
+ alpha=0.7,
1236
+ linewidth=linewidth,
1237
+ transform=fig.transFigure,
1238
+ zorder=100,
1239
+ )
1240
+ fig.patches.append(rect)
1241
+ except Exception:
1242
+ pass
1243
+
1244
+ # Map element types to matplotlib objects
1245
+ element_map = {
1246
+ "title": ax.title,
1247
+ "xlabel": ax.xaxis.label,
1248
+ "ylabel": ax.yaxis.label,
1249
+ }
1250
+
1251
+ # Draw selection highlight (outline only, no fill)
1252
+ select_color = "#FFC850" # Soft warm yellow for outline
1253
+ if selected_type in element_map:
1254
+ add_highlight_box(
1255
+ element_map[selected_type], select_color, 0.0, linewidth=2
1256
+ )
1257
+ elif selected_type == "xaxis":
1258
+ add_spine_highlight(ax.spines["bottom"], select_color, 0.0, linewidth=2)
1259
+ elif selected_type == "yaxis":
1260
+ add_spine_highlight(ax.spines["left"], select_color, 0.0, linewidth=2)
1261
+ elif selected_type == "legend":
1262
+ legend = ax.get_legend()
1263
+ if legend:
1264
+ try:
1265
+ bbox = legend.get_window_extent(renderer)
1266
+ fig_bbox = bbox.transformed(fig.transFigure.inverted())
1267
+ padding = 0.01
1268
+ rect = FancyBboxPatch(
1269
+ (fig_bbox.x0 - padding, fig_bbox.y0 - padding),
1270
+ fig_bbox.width + 2 * padding,
1271
+ fig_bbox.height + 2 * padding,
1272
+ boxstyle="round,pad=0.02",
1273
+ facecolor="none",
1274
+ edgecolor=select_color,
1275
+ alpha=0.7,
1276
+ linewidth=2,
1277
+ transform=fig.transFigure,
1278
+ zorder=100,
1279
+ )
1280
+ fig.patches.append(rect)
1281
+ except Exception:
1282
+ pass
1283
+
1284
+ # Note: Hover highlights are now drawn via fast PIL overlay in _update_hover_overlay()
1285
+
1286
+ def _plot_from_csv(self, ax, o, highlight_trace=None, hover_trace=None):
1287
+ """Reconstruct plot from CSV data using trace info.
1288
+
1289
+ Parameters
1290
+ ----------
1291
+ ax : matplotlib.axes.Axes
1292
+ The axes to plot on
1293
+ o : dict
1294
+ Current overrides containing trace info
1295
+ highlight_trace : int, optional
1296
+ Index of trace to highlight with selection effect (yellow glow)
1297
+ hover_trace : int, optional
1298
+ Index of trace to highlight with hover effect (cyan glow)
1299
+ """
1300
+ import pandas as pd
1301
+ from ._defaults import _normalize_legend_loc
1302
+
1303
+ if not isinstance(self.csv_data, pd.DataFrame):
1304
+ return
1305
+
1306
+ df = self.csv_data
1307
+ linewidth = o.get("linewidth", 1.0)
1308
+ legend_visible = o.get("legend_visible", True)
1309
+ legend_fontsize = o.get("legend_fontsize", 6)
1310
+ legend_frameon = o.get("legend_frameon", False)
1311
+ legend_loc = _normalize_legend_loc(o.get("legend_loc", "best"))
1312
+
1313
+ traces = o.get("traces", [])
1314
+
1315
+ if traces:
1316
+ for i, trace in enumerate(traces):
1317
+ csv_cols = trace.get("csv_columns", {})
1318
+ x_col = csv_cols.get("x")
1319
+ y_col = csv_cols.get("y")
1320
+
1321
+ if x_col in df.columns and y_col in df.columns:
1322
+ trace_linewidth = trace.get("linewidth", linewidth)
1323
+ is_selected = highlight_trace is not None and i == highlight_trace
1324
+ is_hovered = (
1325
+ hover_trace is not None and i == hover_trace and not is_selected
1326
+ )
1327
+
1328
+ # Draw selection glow (yellow, stronger)
1329
+ if is_selected:
1330
+ ax.plot(
1331
+ df[x_col],
1332
+ df[y_col],
1333
+ color="yellow",
1334
+ linewidth=trace_linewidth * 4,
1335
+ alpha=0.5,
1336
+ zorder=0,
1337
+ )
1338
+ # Draw hover glow (cyan, subtler)
1339
+ elif is_hovered:
1340
+ ax.plot(
1341
+ df[x_col],
1342
+ df[y_col],
1343
+ color="cyan",
1344
+ linewidth=trace_linewidth * 3,
1345
+ alpha=0.3,
1346
+ zorder=0,
1347
+ )
1348
+
1349
+ ax.plot(
1350
+ df[x_col],
1351
+ df[y_col],
1352
+ label=trace.get("label", trace.get("id", "")),
1353
+ color=trace.get("color"),
1354
+ linestyle=trace.get("linestyle", "-"),
1355
+ linewidth=trace_linewidth
1356
+ * (1.5 if is_selected else (1.2 if is_hovered else 1.0)),
1357
+ marker=trace.get("marker", None),
1358
+ markersize=trace.get("markersize", 6),
1359
+ zorder=10 if is_selected else (5 if is_hovered else 1),
1360
+ )
1361
+
1362
+ if legend_visible and any(t.get("label") for t in traces):
1363
+ ax.legend(
1364
+ fontsize=legend_fontsize, frameon=legend_frameon, loc=legend_loc
1365
+ )
1366
+ else:
1367
+ # Fallback: parse column names
1368
+ cols = df.columns.tolist()
1369
+ trace_groups = {}
1370
+
1371
+ for col in cols:
1372
+ if col.endswith("_x"):
1373
+ trace_id = col[:-2]
1374
+ y_col = trace_id + "_y"
1375
+ if y_col in cols:
1376
+ parts = trace_id.split("_")
1377
+ label = parts[2] if len(parts) > 2 else trace_id
1378
+ trace_groups[trace_id] = {
1379
+ "x_col": col,
1380
+ "y_col": y_col,
1381
+ "label": label,
1382
+ }
1383
+
1384
+ if trace_groups:
1385
+ for trace_id, info in trace_groups.items():
1386
+ ax.plot(
1387
+ df[info["x_col"]],
1388
+ df[info["y_col"]],
1389
+ label=info["label"],
1390
+ linewidth=linewidth,
1391
+ )
1392
+ if legend_visible:
1393
+ ax.legend(
1394
+ fontsize=legend_fontsize, frameon=legend_frameon, loc=legend_loc
1395
+ )
1396
+ elif len(cols) >= 2:
1397
+ x_col = cols[0]
1398
+ for y_col in cols[1:]:
1399
+ try:
1400
+ ax.plot(
1401
+ df[x_col], df[y_col], label=str(y_col), linewidth=linewidth
1402
+ )
1403
+ except Exception:
1404
+ pass
1405
+ if len(cols) > 2 and legend_visible:
1406
+ ax.legend(
1407
+ fontsize=legend_fontsize, frameon=legend_frameon, loc=legend_loc
1408
+ )
1409
+
1410
+ def _get_trace_labels(self):
1411
+ """Get list of trace labels for selection combo."""
1412
+ traces = self.current_overrides.get("traces", [])
1413
+ if not traces:
1414
+ return ["(no traces)"]
1415
+ return [t.get("label", t.get("id", f"Trace {i}")) for i, t in enumerate(traces)]
1416
+
1417
+ def _get_all_element_labels(self):
1418
+ """Get list of all selectable element labels."""
1419
+ labels = []
1420
+
1421
+ # Fixed elements
1422
+ labels.append("Title")
1423
+ labels.append("X Label")
1424
+ labels.append("Y Label")
1425
+ labels.append("X Axis")
1426
+ labels.append("Y Axis")
1427
+ labels.append("Legend")
1428
+
1429
+ # Traces
1430
+ traces = self.current_overrides.get("traces", [])
1431
+ for i, t in enumerate(traces):
1432
+ label = t.get("label", t.get("id", f"Trace {i}"))
1433
+ labels.append(f"Trace: {label}")
1434
+
1435
+ return labels
1436
+
1437
+ def _on_preview_click(self, sender, app_data):
1438
+ """Handle click on preview image to select element."""
1439
+ import dearpygui.dearpygui as dpg
1440
+
1441
+ # Only handle left clicks
1442
+ if app_data != 0: # 0 = left button
1443
+ return
1444
+
1445
+ # Get mouse position relative to viewport
1446
+ mouse_pos = dpg.get_mouse_pos(local=False)
1447
+
1448
+ # Get preview image position and size
1449
+ if not dpg.does_item_exist("preview_image"):
1450
+ return
1451
+
1452
+ # Get the image item's position in the window
1453
+ img_pos = dpg.get_item_pos("preview_image")
1454
+ panel_pos = dpg.get_item_pos("preview_panel")
1455
+
1456
+ # Calculate click position relative to image
1457
+ click_x = mouse_pos[0] - panel_pos[0] - img_pos[0]
1458
+ click_y = mouse_pos[1] - panel_pos[1] - img_pos[1]
1459
+
1460
+ # Check if click is within image bounds (800x600)
1461
+ max_width, max_height = 800, 600
1462
+ if not (0 <= click_x <= max_width and 0 <= click_y <= max_height):
1463
+ return
1464
+
1465
+ # First check if click is on a fixed element (title, labels, axes, legend)
1466
+ element = self._find_clicked_element(click_x, click_y)
1467
+
1468
+ if element:
1469
+ self._select_element(element, dpg)
1470
+ else:
1471
+ # Fall back to trace selection
1472
+ trace_idx = self._find_nearest_trace(
1473
+ click_x, click_y, max_width, max_height
1474
+ )
1475
+ if trace_idx is not None:
1476
+ self._select_element({"type": "trace", "index": trace_idx}, dpg)
1477
+
1478
+ def _on_preview_hover(self, sender, app_data):
1479
+ """Handle mouse move for hover effects on preview (optimized with caching)."""
1480
+ import dearpygui.dearpygui as dpg
1481
+ import time
1482
+
1483
+ # Throttle hover updates - reduced to 16ms (~60fps) since we use fast overlay
1484
+ current_time = time.time()
1485
+ if current_time - self._last_hover_check < 0.016:
1486
+ return
1487
+ self._last_hover_check = current_time
1488
+
1489
+ # Get mouse position relative to viewport
1490
+ mouse_pos = dpg.get_mouse_pos(local=False)
1491
+
1492
+ # Get preview image position
1493
+ if not dpg.does_item_exist("preview_image"):
1494
+ return
1495
+
1496
+ img_pos = dpg.get_item_pos("preview_image")
1497
+ panel_pos = dpg.get_item_pos("preview_panel")
1498
+
1499
+ # Calculate hover position relative to image
1500
+ hover_x = mouse_pos[0] - panel_pos[0] - img_pos[0]
1501
+ hover_y = mouse_pos[1] - panel_pos[1] - img_pos[1]
1502
+
1503
+ # Check if within image bounds
1504
+ max_width, max_height = 800, 600
1505
+ if not (0 <= hover_x <= max_width and 0 <= hover_y <= max_height):
1506
+ if self._hovered_element is not None:
1507
+ self._hovered_element = None
1508
+ dpg.set_value("hover_text", "")
1509
+ # Use fast overlay update instead of full redraw
1510
+ self._update_hover_overlay(dpg)
1511
+ return
1512
+
1513
+ # Find element under cursor
1514
+ element = self._find_clicked_element(hover_x, hover_y)
1515
+
1516
+ if element is None:
1517
+ # Check for trace hover
1518
+ trace_idx = self._find_nearest_trace(
1519
+ hover_x, hover_y, max_width, max_height
1520
+ )
1521
+ if trace_idx is not None:
1522
+ element = {"type": "trace", "index": trace_idx}
1523
+
1524
+ # Check if hover changed
1525
+ old_hover = self._hovered_element
1526
+ if element != old_hover:
1527
+ self._hovered_element = element
1528
+ if element:
1529
+ elem_type = element.get("type", "")
1530
+ elem_idx = element.get("index")
1531
+ if elem_type == "trace" and elem_idx is not None:
1532
+ traces = self.current_overrides.get("traces", [])
1533
+ if elem_idx < len(traces):
1534
+ label = traces[elem_idx].get("label", f"Trace {elem_idx}")
1535
+ dpg.set_value("hover_text", f"Hover: {label} (click to select)")
1536
+ else:
1537
+ label = elem_type.replace("x", "X ").replace("y", "Y ").title()
1538
+ dpg.set_value("hover_text", f"Hover: {label} (click to select)")
1539
+ else:
1540
+ dpg.set_value("hover_text", "")
1541
+
1542
+ # Use fast overlay update for hover (no matplotlib re-render)
1543
+ self._update_hover_overlay(dpg)
1544
+
1545
+ def _find_clicked_element(self, click_x, click_y):
1546
+ """Find which element was clicked based on stored bboxes."""
1547
+ if not self._element_bboxes:
1548
+ return None
1549
+
1550
+ # Check each element bbox
1551
+ for element_type, bbox in self._element_bboxes.items():
1552
+ if bbox is None:
1553
+ continue
1554
+ x0, y0, x1, y1 = bbox
1555
+ if x0 <= click_x <= x1 and y0 <= click_y <= y1:
1556
+ return {"type": element_type, "index": None}
1557
+
1558
+ return None
1559
+
1560
+ def _select_element(self, element, dpg):
1561
+ """Select an element and show appropriate controls."""
1562
+ self._selected_element = element
1563
+ elem_type = element.get("type")
1564
+ elem_idx = element.get("index")
1565
+
1566
+ # Hide all control groups first
1567
+ dpg.configure_item("trace_controls_group", show=False)
1568
+ dpg.configure_item("text_controls_group", show=False)
1569
+ dpg.configure_item("axis_controls_group", show=False)
1570
+ dpg.configure_item("legend_controls_group", show=False)
1571
+
1572
+ # Update combo selection
1573
+ if elem_type == "trace":
1574
+ traces = self.current_overrides.get("traces", [])
1575
+ if elem_idx is not None and elem_idx < len(traces):
1576
+ trace = traces[elem_idx]
1577
+ label = (
1578
+ f"Trace: {trace.get('label', trace.get('id', f'Trace {elem_idx}'))}"
1579
+ )
1580
+ dpg.set_value("element_selector_combo", label)
1581
+
1582
+ # Show trace controls and populate
1583
+ dpg.configure_item("trace_controls_group", show=True)
1584
+ self._selected_trace_index = elem_idx
1585
+ dpg.set_value("trace_label_input", trace.get("label", ""))
1586
+
1587
+ color_hex = trace.get("color", "#0080bf")
1588
+ try:
1589
+ r = int(color_hex[1:3], 16)
1590
+ g = int(color_hex[3:5], 16)
1591
+ b = int(color_hex[5:7], 16)
1592
+ dpg.set_value("trace_color_picker", [r, g, b])
1593
+ except (ValueError, IndexError):
1594
+ dpg.set_value("trace_color_picker", [128, 128, 191])
1595
+
1596
+ dpg.set_value("trace_linewidth_slider", trace.get("linewidth", 1.0))
1597
+ dpg.set_value("trace_linestyle_combo", trace.get("linestyle", "-"))
1598
+ dpg.set_value("trace_marker_combo", trace.get("marker", "") or "")
1599
+ dpg.set_value("trace_markersize_slider", trace.get("markersize", 6.0))
1600
+
1601
+ dpg.set_value(
1602
+ "selection_text",
1603
+ f"Selected: {trace.get('label', f'Trace {elem_idx}')}",
1604
+ )
1605
+
1606
+ elif elem_type in ("title", "xlabel", "ylabel"):
1607
+ dpg.set_value(
1608
+ "element_selector_combo",
1609
+ elem_type.replace("x", "X ").replace("y", "Y ").title(),
1610
+ )
1611
+ dpg.configure_item("text_controls_group", show=True)
1612
+
1613
+ o = self.current_overrides
1614
+ if elem_type == "title":
1615
+ dpg.set_value("element_text_input", o.get("title", ""))
1616
+ dpg.set_value("element_fontsize_slider", o.get("title_fontsize", 8))
1617
+ elif elem_type == "xlabel":
1618
+ dpg.set_value("element_text_input", o.get("xlabel", ""))
1619
+ dpg.set_value("element_fontsize_slider", o.get("axis_fontsize", 7))
1620
+ elif elem_type == "ylabel":
1621
+ dpg.set_value("element_text_input", o.get("ylabel", ""))
1622
+ dpg.set_value("element_fontsize_slider", o.get("axis_fontsize", 7))
1623
+
1624
+ dpg.set_value("selection_text", f"Selected: {elem_type.title()}")
1625
+
1626
+ elif elem_type in ("xaxis", "yaxis"):
1627
+ label = "X Axis" if elem_type == "xaxis" else "Y Axis"
1628
+ dpg.set_value("element_selector_combo", label)
1629
+ dpg.configure_item("axis_controls_group", show=True)
1630
+
1631
+ o = self.current_overrides
1632
+ dpg.set_value("axis_linewidth_slider", o.get("axis_width", 0.2))
1633
+ dpg.set_value("axis_tick_length_slider", o.get("tick_length", 0.8))
1634
+ dpg.set_value("axis_tick_fontsize_slider", o.get("tick_fontsize", 7))
1635
+
1636
+ if elem_type == "xaxis":
1637
+ dpg.set_value(
1638
+ "axis_show_spine_checkbox", not o.get("hide_bottom_spine", False)
1639
+ )
1640
+ else:
1641
+ dpg.set_value(
1642
+ "axis_show_spine_checkbox", not o.get("hide_left_spine", False)
1643
+ )
1644
+
1645
+ dpg.set_value("selection_text", f"Selected: {label}")
1646
+
1647
+ elif elem_type == "legend":
1648
+ dpg.set_value("element_selector_combo", "Legend")
1649
+ dpg.configure_item("legend_controls_group", show=True)
1650
+
1651
+ o = self.current_overrides
1652
+ dpg.set_value("legend_visible_edit", o.get("legend_visible", True))
1653
+ dpg.set_value("legend_frameon_edit", o.get("legend_frameon", False))
1654
+ dpg.set_value("legend_loc_edit", o.get("legend_loc", "best"))
1655
+ dpg.set_value("legend_fontsize_edit", o.get("legend_fontsize", 6))
1656
+
1657
+ dpg.set_value("selection_text", "Selected: Legend")
1658
+
1659
+ # Redraw with highlight
1660
+ self._update_preview(dpg)
1661
+
1662
+ def _on_element_selected(self, sender, app_data):
1663
+ """Handle element selection from combo box."""
1664
+ import dearpygui.dearpygui as dpg
1665
+
1666
+ if app_data == "Title":
1667
+ self._select_element({"type": "title", "index": None}, dpg)
1668
+ elif app_data == "X Label":
1669
+ self._select_element({"type": "xlabel", "index": None}, dpg)
1670
+ elif app_data == "Y Label":
1671
+ self._select_element({"type": "ylabel", "index": None}, dpg)
1672
+ elif app_data == "X Axis":
1673
+ self._select_element({"type": "xaxis", "index": None}, dpg)
1674
+ elif app_data == "Y Axis":
1675
+ self._select_element({"type": "yaxis", "index": None}, dpg)
1676
+ elif app_data == "Legend":
1677
+ self._select_element({"type": "legend", "index": None}, dpg)
1678
+ elif app_data.startswith("Trace: "):
1679
+ # Find trace index
1680
+ trace_label = app_data[7:] # Remove "Trace: " prefix
1681
+ traces = self.current_overrides.get("traces", [])
1682
+ for i, t in enumerate(traces):
1683
+ if t.get("label", t.get("id", f"Trace {i}")) == trace_label:
1684
+ self._select_element({"type": "trace", "index": i}, dpg)
1685
+ break
1686
+
1687
+ def _on_text_element_change(self, sender, app_data, user_data=None):
1688
+ """Handle changes to text element properties."""
1689
+ import dearpygui.dearpygui as dpg
1690
+
1691
+ if self._selected_element is None:
1692
+ return
1693
+
1694
+ elem_type = self._selected_element.get("type")
1695
+ if elem_type not in ("title", "xlabel", "ylabel"):
1696
+ return
1697
+
1698
+ text = dpg.get_value("element_text_input")
1699
+ fontsize = dpg.get_value("element_fontsize_slider")
1700
+
1701
+ if elem_type == "title":
1702
+ self.current_overrides["title"] = text
1703
+ self.current_overrides["title_fontsize"] = fontsize
1704
+ elif elem_type == "xlabel":
1705
+ self.current_overrides["xlabel"] = text
1706
+ self.current_overrides["axis_fontsize"] = fontsize
1707
+ elif elem_type == "ylabel":
1708
+ self.current_overrides["ylabel"] = text
1709
+ self.current_overrides["axis_fontsize"] = fontsize
1710
+
1711
+ self._user_modified = True
1712
+ self._update_preview(dpg)
1713
+
1714
+ def _on_axis_element_change(self, sender, app_data, user_data=None):
1715
+ """Handle changes to axis element properties."""
1716
+ import dearpygui.dearpygui as dpg
1717
+
1718
+ if self._selected_element is None:
1719
+ return
1720
+
1721
+ elem_type = self._selected_element.get("type")
1722
+ if elem_type not in ("xaxis", "yaxis"):
1723
+ return
1724
+
1725
+ self.current_overrides["axis_width"] = dpg.get_value("axis_linewidth_slider")
1726
+ self.current_overrides["tick_length"] = dpg.get_value("axis_tick_length_slider")
1727
+ self.current_overrides["tick_fontsize"] = dpg.get_value(
1728
+ "axis_tick_fontsize_slider"
1729
+ )
1730
+
1731
+ show_spine = dpg.get_value("axis_show_spine_checkbox")
1732
+ if elem_type == "xaxis":
1733
+ self.current_overrides["hide_bottom_spine"] = not show_spine
1734
+ else:
1735
+ self.current_overrides["hide_left_spine"] = not show_spine
1736
+
1737
+ self._user_modified = True
1738
+ self._update_preview(dpg)
1739
+
1740
+ def _on_legend_element_change(self, sender, app_data, user_data=None):
1741
+ """Handle changes to legend element properties."""
1742
+ import dearpygui.dearpygui as dpg
1743
+
1744
+ if self._selected_element is None:
1745
+ return
1746
+
1747
+ elem_type = self._selected_element.get("type")
1748
+ if elem_type != "legend":
1749
+ return
1750
+
1751
+ self.current_overrides["legend_visible"] = dpg.get_value("legend_visible_edit")
1752
+ self.current_overrides["legend_frameon"] = dpg.get_value("legend_frameon_edit")
1753
+ self.current_overrides["legend_loc"] = dpg.get_value("legend_loc_edit")
1754
+ self.current_overrides["legend_fontsize"] = dpg.get_value(
1755
+ "legend_fontsize_edit"
1756
+ )
1757
+
1758
+ self._user_modified = True
1759
+ self._update_preview(dpg)
1760
+
1761
+ def _deselect_element(self, sender=None, app_data=None, user_data=None):
1762
+ """Deselect the current element."""
1763
+ import dearpygui.dearpygui as dpg
1764
+
1765
+ self._selected_element = None
1766
+ self._selected_trace_index = None
1767
+
1768
+ # Hide all control groups
1769
+ dpg.configure_item("trace_controls_group", show=False)
1770
+ dpg.configure_item("text_controls_group", show=False)
1771
+ dpg.configure_item("axis_controls_group", show=False)
1772
+ dpg.configure_item("legend_controls_group", show=False)
1773
+
1774
+ dpg.set_value("selection_text", "")
1775
+ dpg.set_value("element_selector_combo", "")
1776
+ self._update_preview(dpg)
1777
+
1778
+ def _find_nearest_trace(self, click_x, click_y, preview_width, preview_height):
1779
+ """Find the nearest trace to the click position."""
1780
+ import pandas as pd
1781
+ import numpy as np
1782
+
1783
+ if self.csv_data is None or not isinstance(self.csv_data, pd.DataFrame):
1784
+ return None
1785
+
1786
+ traces = self.current_overrides.get("traces", [])
1787
+ if not traces:
1788
+ return None
1789
+
1790
+ # Get preview bounds from last render
1791
+ if self._preview_bounds is None:
1792
+ return None
1793
+
1794
+ x_offset, y_offset, fig_width, fig_height = self._preview_bounds
1795
+
1796
+ # Adjust click coordinates to figure space
1797
+ fig_x = click_x - x_offset
1798
+ fig_y = click_y - y_offset
1799
+
1800
+ # Check if click is within figure bounds
1801
+ if not (0 <= fig_x <= fig_width and 0 <= fig_y <= fig_height):
1802
+ return None
1803
+
1804
+ # Get axes transform info
1805
+ if self._axes_transform is None:
1806
+ return None
1807
+
1808
+ ax_x0, ax_y0, ax_width, ax_height, xlim, ylim = self._axes_transform
1809
+
1810
+ # Convert figure pixel to axes pixel
1811
+ ax_pixel_x = fig_x - ax_x0
1812
+ ax_pixel_y = fig_y - ax_y0
1813
+
1814
+ # Check if click is within axes bounds
1815
+ if not (0 <= ax_pixel_x <= ax_width and 0 <= ax_pixel_y <= ax_height):
1816
+ return None
1817
+
1818
+ # Convert axes pixel to data coordinates
1819
+ # Note: y is flipped (0 at top in pixel space)
1820
+ data_x = xlim[0] + (ax_pixel_x / ax_width) * (xlim[1] - xlim[0])
1821
+ data_y = ylim[1] - (ax_pixel_y / ax_height) * (ylim[1] - ylim[0])
1822
+
1823
+ # Find nearest trace
1824
+ df = self.csv_data
1825
+ min_dist = float("inf")
1826
+ nearest_idx = None
1827
+
1828
+ for i, trace in enumerate(traces):
1829
+ csv_cols = trace.get("csv_columns", {})
1830
+ x_col = csv_cols.get("x")
1831
+ y_col = csv_cols.get("y")
1832
+
1833
+ if x_col not in df.columns or y_col not in df.columns:
1834
+ continue
1835
+
1836
+ trace_x = df[x_col].dropna().values
1837
+ trace_y = df[y_col].dropna().values
1838
+
1839
+ if len(trace_x) == 0:
1840
+ continue
1841
+
1842
+ # Normalize coordinates for distance calculation
1843
+ x_range = xlim[1] - xlim[0]
1844
+ y_range = ylim[1] - ylim[0]
1845
+
1846
+ norm_click_x = (data_x - xlim[0]) / x_range if x_range > 0 else 0
1847
+ norm_click_y = (data_y - ylim[0]) / y_range if y_range > 0 else 0
1848
+
1849
+ norm_trace_x = (trace_x - xlim[0]) / x_range if x_range > 0 else trace_x
1850
+ norm_trace_y = (trace_y - ylim[0]) / y_range if y_range > 0 else trace_y
1851
+
1852
+ # Calculate distances to all points
1853
+ distances = np.sqrt(
1854
+ (norm_trace_x - norm_click_x) ** 2 + (norm_trace_y - norm_click_y) ** 2
1855
+ )
1856
+ min_trace_dist = np.min(distances)
1857
+
1858
+ if min_trace_dist < min_dist:
1859
+ min_dist = min_trace_dist
1860
+ nearest_idx = i
1861
+
1862
+ # Only select if close enough (threshold in normalized space)
1863
+ if min_dist < 0.1: # 10% of plot area
1864
+ return nearest_idx
1865
+
1866
+ return None
1867
+
1868
+ def _on_trace_property_change(self, sender, app_data, user_data=None):
1869
+ """Handle changes to selected trace properties."""
1870
+ import dearpygui.dearpygui as dpg
1871
+
1872
+ if self._selected_trace_index is None:
1873
+ return
1874
+
1875
+ traces = self.current_overrides.get("traces", [])
1876
+ if self._selected_trace_index >= len(traces):
1877
+ return
1878
+
1879
+ trace = traces[self._selected_trace_index]
1880
+
1881
+ # Update trace properties from widgets
1882
+ trace["label"] = dpg.get_value("trace_label_input")
1883
+
1884
+ # Convert RGB to hex
1885
+ color_rgb = dpg.get_value("trace_color_picker")
1886
+ if color_rgb and len(color_rgb) >= 3:
1887
+ r, g, b = int(color_rgb[0]), int(color_rgb[1]), int(color_rgb[2])
1888
+ trace["color"] = f"#{r:02x}{g:02x}{b:02x}"
1889
+
1890
+ trace["linewidth"] = dpg.get_value("trace_linewidth_slider")
1891
+ trace["linestyle"] = dpg.get_value("trace_linestyle_combo")
1892
+
1893
+ marker = dpg.get_value("trace_marker_combo")
1894
+ trace["marker"] = marker if marker else None
1895
+
1896
+ trace["markersize"] = dpg.get_value("trace_markersize_slider")
1897
+
1898
+ self._user_modified = True
1899
+ self._update_preview(dpg)
1900
+
1901
+ def _save_manual(self, sender=None, app_data=None, user_data=None):
1902
+ """Save current overrides to .manual.json."""
1903
+ import dearpygui.dearpygui as dpg
1904
+ from ._edit import save_manual_overrides
1905
+
1906
+ try:
1907
+ self._collect_overrides(dpg)
1908
+ manual_path = save_manual_overrides(self.json_path, self.current_overrides)
1909
+ dpg.set_value("status_text", f"Saved: {manual_path.name}")
1910
+ except Exception as e:
1911
+ dpg.set_value("status_text", f"Error: {str(e)}")
1912
+
1913
+ def _reset_overrides(self, sender=None, app_data=None, user_data=None):
1914
+ """Reset to initial overrides."""
1915
+ import dearpygui.dearpygui as dpg
1916
+
1917
+ self.current_overrides = copy.deepcopy(self._initial_overrides)
1918
+ self._user_modified = False
1919
+
1920
+ # Update all widgets
1921
+ dpg.set_value("title_input", self.current_overrides.get("title", ""))
1922
+ dpg.set_value("xlabel_input", self.current_overrides.get("xlabel", ""))
1923
+ dpg.set_value("ylabel_input", self.current_overrides.get("ylabel", ""))
1924
+ dpg.set_value("linewidth_slider", self.current_overrides.get("linewidth", 1.0))
1925
+ dpg.set_value("grid_checkbox", self.current_overrides.get("grid", False))
1926
+ dpg.set_value(
1927
+ "transparent_checkbox", self.current_overrides.get("transparent", True)
1928
+ )
1929
+
1930
+ self._update_preview(dpg)
1931
+ dpg.set_value("status_text", "Reset to original")
1932
+
1933
+ def _export_png(self, sender=None, app_data=None, user_data=None):
1934
+ """Export current view to PNG."""
1935
+ import dearpygui.dearpygui as dpg
1936
+ import matplotlib
1937
+
1938
+ matplotlib.use("Agg")
1939
+ import matplotlib.pyplot as plt
1940
+
1941
+ try:
1942
+ self._collect_overrides(dpg)
1943
+ output_path = self.json_path.with_suffix(".edited.png")
1944
+
1945
+ # Full resolution render
1946
+ o = self.current_overrides
1947
+ fig_size = o.get("fig_size", [3.15, 2.68])
1948
+ dpi = o.get("dpi", 300)
1949
+
1950
+ fig, ax = plt.subplots(figsize=fig_size, dpi=dpi)
1951
+
1952
+ if self.csv_data is not None:
1953
+ self._plot_from_csv(ax, o)
1954
+
1955
+ if o.get("title"):
1956
+ ax.set_title(o["title"], fontsize=o.get("title_fontsize", 8))
1957
+ if o.get("xlabel"):
1958
+ ax.set_xlabel(o["xlabel"], fontsize=o.get("axis_fontsize", 7))
1959
+ if o.get("ylabel"):
1960
+ ax.set_ylabel(o["ylabel"], fontsize=o.get("axis_fontsize", 7))
1961
+
1962
+ fig.tight_layout()
1963
+ fig.savefig(
1964
+ output_path,
1965
+ dpi=dpi,
1966
+ bbox_inches="tight",
1967
+ transparent=o.get("transparent", True),
1968
+ )
1969
+ plt.close(fig)
1970
+
1971
+ dpg.set_value("status_text", f"Exported: {output_path.name}")
1972
+ except Exception as e:
1973
+ dpg.set_value("status_text", f"Error: {str(e)}")
1974
+
1975
+
1976
+ # EOF