scitex 2.14.0__py3-none-any.whl → 2.15.2__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 (300) hide show
  1. scitex/__init__.py +71 -17
  2. scitex/_env_loader.py +156 -0
  3. scitex/_mcp_resources/__init__.py +37 -0
  4. scitex/_mcp_resources/_cheatsheet.py +135 -0
  5. scitex/_mcp_resources/_figrecipe.py +138 -0
  6. scitex/_mcp_resources/_formats.py +102 -0
  7. scitex/_mcp_resources/_modules.py +337 -0
  8. scitex/_mcp_resources/_session.py +149 -0
  9. scitex/_mcp_tools/__init__.py +4 -0
  10. scitex/_mcp_tools/audio.py +66 -0
  11. scitex/_mcp_tools/diagram.py +11 -95
  12. scitex/_mcp_tools/introspect.py +210 -0
  13. scitex/_mcp_tools/plt.py +260 -305
  14. scitex/_mcp_tools/scholar.py +74 -0
  15. scitex/_mcp_tools/social.py +244 -0
  16. scitex/_mcp_tools/template.py +24 -0
  17. scitex/_mcp_tools/writer.py +21 -204
  18. scitex/ai/_gen_ai/_PARAMS.py +10 -7
  19. scitex/ai/classification/reporters/_SingleClassificationReporter.py +45 -1603
  20. scitex/ai/classification/reporters/_mixins/__init__.py +36 -0
  21. scitex/ai/classification/reporters/_mixins/_constants.py +67 -0
  22. scitex/ai/classification/reporters/_mixins/_cv_summary.py +387 -0
  23. scitex/ai/classification/reporters/_mixins/_feature_importance.py +119 -0
  24. scitex/ai/classification/reporters/_mixins/_metrics.py +275 -0
  25. scitex/ai/classification/reporters/_mixins/_plotting.py +179 -0
  26. scitex/ai/classification/reporters/_mixins/_reports.py +153 -0
  27. scitex/ai/classification/reporters/_mixins/_storage.py +160 -0
  28. scitex/ai/classification/timeseries/_TimeSeriesSlidingWindowSplit.py +30 -1550
  29. scitex/ai/classification/timeseries/_sliding_window_core.py +467 -0
  30. scitex/ai/classification/timeseries/_sliding_window_plotting.py +369 -0
  31. scitex/audio/README.md +40 -36
  32. scitex/audio/__init__.py +129 -61
  33. scitex/audio/_branding.py +185 -0
  34. scitex/audio/_mcp/__init__.py +32 -0
  35. scitex/audio/_mcp/handlers.py +59 -6
  36. scitex/audio/_mcp/speak_handlers.py +238 -0
  37. scitex/audio/_relay.py +225 -0
  38. scitex/audio/_tts.py +18 -10
  39. scitex/audio/engines/base.py +17 -10
  40. scitex/audio/engines/elevenlabs_engine.py +7 -2
  41. scitex/audio/mcp_server.py +228 -75
  42. scitex/canvas/README.md +1 -1
  43. scitex/canvas/editor/_dearpygui/__init__.py +25 -0
  44. scitex/canvas/editor/_dearpygui/_editor.py +147 -0
  45. scitex/canvas/editor/_dearpygui/_handlers.py +476 -0
  46. scitex/canvas/editor/_dearpygui/_panels/__init__.py +17 -0
  47. scitex/canvas/editor/_dearpygui/_panels/_control.py +119 -0
  48. scitex/canvas/editor/_dearpygui/_panels/_element_controls.py +190 -0
  49. scitex/canvas/editor/_dearpygui/_panels/_preview.py +43 -0
  50. scitex/canvas/editor/_dearpygui/_panels/_sections.py +390 -0
  51. scitex/canvas/editor/_dearpygui/_plotting.py +187 -0
  52. scitex/canvas/editor/_dearpygui/_rendering.py +504 -0
  53. scitex/canvas/editor/_dearpygui/_selection.py +295 -0
  54. scitex/canvas/editor/_dearpygui/_state.py +93 -0
  55. scitex/canvas/editor/_dearpygui/_utils.py +61 -0
  56. scitex/canvas/editor/flask_editor/_core/__init__.py +27 -0
  57. scitex/canvas/editor/flask_editor/_core/_bbox_extraction.py +200 -0
  58. scitex/canvas/editor/flask_editor/_core/_editor.py +173 -0
  59. scitex/canvas/editor/flask_editor/_core/_export_helpers.py +353 -0
  60. scitex/canvas/editor/flask_editor/_core/_routes_basic.py +190 -0
  61. scitex/canvas/editor/flask_editor/_core/_routes_export.py +332 -0
  62. scitex/canvas/editor/flask_editor/_core/_routes_panels.py +252 -0
  63. scitex/canvas/editor/flask_editor/_core/_routes_save.py +218 -0
  64. scitex/canvas/editor/flask_editor/_core.py +25 -1684
  65. scitex/canvas/editor/flask_editor/templates/__init__.py +32 -70
  66. scitex/cli/__init__.py +38 -43
  67. scitex/cli/audio.py +76 -27
  68. scitex/cli/capture.py +13 -20
  69. scitex/cli/introspect.py +481 -0
  70. scitex/cli/main.py +200 -109
  71. scitex/cli/mcp.py +60 -34
  72. scitex/cli/plt.py +357 -0
  73. scitex/cli/repro.py +15 -8
  74. scitex/cli/resource.py +15 -8
  75. scitex/cli/scholar/__init__.py +23 -8
  76. scitex/cli/scholar/_crossref_scitex.py +296 -0
  77. scitex/cli/scholar/_fetch.py +25 -3
  78. scitex/cli/social.py +314 -0
  79. scitex/cli/stats.py +15 -8
  80. scitex/cli/template.py +129 -12
  81. scitex/cli/tex.py +15 -8
  82. scitex/cli/writer.py +132 -8
  83. scitex/cloud/__init__.py +41 -2
  84. scitex/config/README.md +1 -1
  85. scitex/config/__init__.py +16 -2
  86. scitex/config/_env_registry.py +256 -0
  87. scitex/context/__init__.py +22 -0
  88. scitex/dev/__init__.py +20 -1
  89. scitex/diagram/__init__.py +42 -19
  90. scitex/diagram/mcp_server.py +13 -125
  91. scitex/gen/__init__.py +50 -14
  92. scitex/gen/_list_packages.py +4 -4
  93. scitex/introspect/__init__.py +82 -0
  94. scitex/introspect/_call_graph.py +303 -0
  95. scitex/introspect/_class_hierarchy.py +163 -0
  96. scitex/introspect/_core.py +41 -0
  97. scitex/introspect/_docstring.py +131 -0
  98. scitex/introspect/_examples.py +113 -0
  99. scitex/introspect/_imports.py +271 -0
  100. scitex/{gen/_inspect_module.py → introspect/_list_api.py} +43 -54
  101. scitex/introspect/_mcp/__init__.py +41 -0
  102. scitex/introspect/_mcp/handlers.py +233 -0
  103. scitex/introspect/_members.py +155 -0
  104. scitex/introspect/_resolve.py +89 -0
  105. scitex/introspect/_signature.py +131 -0
  106. scitex/introspect/_source.py +80 -0
  107. scitex/introspect/_type_hints.py +172 -0
  108. scitex/io/_save.py +1 -2
  109. scitex/io/bundle/README.md +1 -1
  110. scitex/logging/_formatters.py +19 -9
  111. scitex/mcp_server.py +98 -5
  112. scitex/os/__init__.py +4 -0
  113. scitex/{gen → os}/_check_host.py +4 -5
  114. scitex/plt/__init__.py +245 -550
  115. scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin/_wrappers.py +5 -10
  116. scitex/plt/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
  117. scitex/plt/gallery/README.md +1 -1
  118. scitex/plt/utils/_hitmap/__init__.py +82 -0
  119. scitex/plt/utils/_hitmap/_artist_extraction.py +343 -0
  120. scitex/plt/utils/_hitmap/_color_application.py +346 -0
  121. scitex/plt/utils/_hitmap/_color_conversion.py +121 -0
  122. scitex/plt/utils/_hitmap/_constants.py +40 -0
  123. scitex/plt/utils/_hitmap/_hitmap_core.py +334 -0
  124. scitex/plt/utils/_hitmap/_path_extraction.py +357 -0
  125. scitex/plt/utils/_hitmap/_query.py +113 -0
  126. scitex/plt/utils/_hitmap.py +46 -1616
  127. scitex/plt/utils/_metadata/__init__.py +80 -0
  128. scitex/plt/utils/_metadata/_artists/__init__.py +25 -0
  129. scitex/plt/utils/_metadata/_artists/_base.py +195 -0
  130. scitex/plt/utils/_metadata/_artists/_collections.py +356 -0
  131. scitex/plt/utils/_metadata/_artists/_extract.py +57 -0
  132. scitex/plt/utils/_metadata/_artists/_images.py +80 -0
  133. scitex/plt/utils/_metadata/_artists/_lines.py +261 -0
  134. scitex/plt/utils/_metadata/_artists/_patches.py +247 -0
  135. scitex/plt/utils/_metadata/_artists/_text.py +106 -0
  136. scitex/plt/utils/_metadata/_csv.py +416 -0
  137. scitex/plt/utils/_metadata/_detect.py +225 -0
  138. scitex/plt/utils/_metadata/_legend.py +127 -0
  139. scitex/plt/utils/_metadata/_rounding.py +117 -0
  140. scitex/plt/utils/_metadata/_verification.py +202 -0
  141. scitex/schema/README.md +1 -1
  142. scitex/scholar/__init__.py +8 -0
  143. scitex/scholar/_mcp/crossref_handlers.py +265 -0
  144. scitex/scholar/core/Scholar.py +63 -1700
  145. scitex/scholar/core/_mixins/__init__.py +36 -0
  146. scitex/scholar/core/_mixins/_enrichers.py +270 -0
  147. scitex/scholar/core/_mixins/_library_handlers.py +100 -0
  148. scitex/scholar/core/_mixins/_loaders.py +103 -0
  149. scitex/scholar/core/_mixins/_pdf_download.py +375 -0
  150. scitex/scholar/core/_mixins/_pipeline.py +312 -0
  151. scitex/scholar/core/_mixins/_project_handlers.py +125 -0
  152. scitex/scholar/core/_mixins/_savers.py +69 -0
  153. scitex/scholar/core/_mixins/_search.py +103 -0
  154. scitex/scholar/core/_mixins/_services.py +88 -0
  155. scitex/scholar/core/_mixins/_url_finding.py +105 -0
  156. scitex/scholar/crossref_scitex.py +367 -0
  157. scitex/scholar/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
  158. scitex/scholar/examples/00_run_all.sh +120 -0
  159. scitex/scholar/jobs/_executors.py +27 -3
  160. scitex/scholar/pdf_download/ScholarPDFDownloader.py +38 -416
  161. scitex/scholar/pdf_download/_cli.py +154 -0
  162. scitex/scholar/pdf_download/strategies/__init__.py +11 -8
  163. scitex/scholar/pdf_download/strategies/manual_download_fallback.py +80 -3
  164. scitex/scholar/pipelines/ScholarPipelineBibTeX.py +73 -121
  165. scitex/scholar/pipelines/ScholarPipelineParallel.py +80 -138
  166. scitex/scholar/pipelines/ScholarPipelineSingle.py +43 -63
  167. scitex/scholar/pipelines/_single_steps.py +71 -36
  168. scitex/scholar/storage/_LibraryManager.py +97 -1695
  169. scitex/scholar/storage/_mixins/__init__.py +30 -0
  170. scitex/scholar/storage/_mixins/_bibtex_handlers.py +128 -0
  171. scitex/scholar/storage/_mixins/_library_operations.py +218 -0
  172. scitex/scholar/storage/_mixins/_metadata_conversion.py +226 -0
  173. scitex/scholar/storage/_mixins/_paper_saving.py +456 -0
  174. scitex/scholar/storage/_mixins/_resolution.py +376 -0
  175. scitex/scholar/storage/_mixins/_storage_helpers.py +121 -0
  176. scitex/scholar/storage/_mixins/_symlink_handlers.py +226 -0
  177. scitex/scholar/url_finder/.tmp/open_url/KNOWN_RESOLVERS.py +462 -0
  178. scitex/scholar/url_finder/.tmp/open_url/README.md +223 -0
  179. scitex/scholar/url_finder/.tmp/open_url/_DOIToURLResolver.py +694 -0
  180. scitex/scholar/url_finder/.tmp/open_url/_OpenURLResolver.py +1160 -0
  181. scitex/scholar/url_finder/.tmp/open_url/_ResolverLinkFinder.py +344 -0
  182. scitex/scholar/url_finder/.tmp/open_url/__init__.py +24 -0
  183. scitex/security/README.md +3 -3
  184. scitex/session/README.md +1 -1
  185. scitex/session/__init__.py +26 -7
  186. scitex/session/_decorator.py +1 -1
  187. scitex/sh/README.md +1 -1
  188. scitex/sh/__init__.py +7 -4
  189. scitex/social/__init__.py +155 -0
  190. scitex/social/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
  191. scitex/stats/_mcp/_handlers/__init__.py +31 -0
  192. scitex/stats/_mcp/_handlers/_corrections.py +113 -0
  193. scitex/stats/_mcp/_handlers/_descriptive.py +78 -0
  194. scitex/stats/_mcp/_handlers/_effect_size.py +106 -0
  195. scitex/stats/_mcp/_handlers/_format.py +94 -0
  196. scitex/stats/_mcp/_handlers/_normality.py +110 -0
  197. scitex/stats/_mcp/_handlers/_posthoc.py +224 -0
  198. scitex/stats/_mcp/_handlers/_power.py +247 -0
  199. scitex/stats/_mcp/_handlers/_recommend.py +102 -0
  200. scitex/stats/_mcp/_handlers/_run_test.py +279 -0
  201. scitex/stats/_mcp/_handlers/_stars.py +48 -0
  202. scitex/stats/_mcp/handlers.py +19 -1171
  203. scitex/stats/auto/_stat_style.py +175 -0
  204. scitex/stats/auto/_style_definitions.py +411 -0
  205. scitex/stats/auto/_styles.py +22 -620
  206. scitex/stats/descriptive/__init__.py +11 -8
  207. scitex/stats/descriptive/_ci.py +39 -0
  208. scitex/stats/power/_power.py +15 -4
  209. scitex/str/__init__.py +2 -1
  210. scitex/str/_title_case.py +63 -0
  211. scitex/template/README.md +1 -1
  212. scitex/template/__init__.py +25 -10
  213. scitex/template/_code_templates.py +147 -0
  214. scitex/template/_mcp/handlers.py +81 -0
  215. scitex/template/_mcp/tool_schemas.py +55 -0
  216. scitex/template/_templates/__init__.py +51 -0
  217. scitex/template/_templates/audio.py +233 -0
  218. scitex/template/_templates/canvas.py +312 -0
  219. scitex/template/_templates/capture.py +268 -0
  220. scitex/template/_templates/config.py +43 -0
  221. scitex/template/_templates/diagram.py +294 -0
  222. scitex/template/_templates/io.py +107 -0
  223. scitex/template/_templates/module.py +53 -0
  224. scitex/template/_templates/plt.py +202 -0
  225. scitex/template/_templates/scholar.py +267 -0
  226. scitex/template/_templates/session.py +130 -0
  227. scitex/template/_templates/session_minimal.py +43 -0
  228. scitex/template/_templates/session_plot.py +67 -0
  229. scitex/template/_templates/session_stats.py +77 -0
  230. scitex/template/_templates/stats.py +323 -0
  231. scitex/template/_templates/writer.py +296 -0
  232. scitex/template/clone_writer_directory.py +5 -5
  233. scitex/ui/_backends/_email.py +10 -2
  234. scitex/ui/_backends/_webhook.py +5 -1
  235. scitex/web/_search_pubmed.py +10 -6
  236. scitex/writer/README.md +1 -1
  237. scitex/writer/_mcp/handlers.py +11 -744
  238. scitex/writer/_mcp/tool_schemas.py +5 -335
  239. scitex-2.15.2.dist-info/METADATA +648 -0
  240. {scitex-2.14.0.dist-info → scitex-2.15.2.dist-info}/RECORD +246 -150
  241. scitex/canvas/editor/flask_editor/templates/_scripts.py +0 -4933
  242. scitex/canvas/editor/flask_editor/templates/_styles.py +0 -1658
  243. scitex/dev/plt/data/mpl/PLOTTING_FUNCTIONS.yaml +0 -90
  244. scitex/dev/plt/data/mpl/PLOTTING_SIGNATURES.yaml +0 -1571
  245. scitex/dev/plt/data/mpl/PLOTTING_SIGNATURES_DETAILED.yaml +0 -6262
  246. scitex/dev/plt/data/mpl/SIGNATURES_FLATTENED.yaml +0 -1274
  247. scitex/dev/plt/data/mpl/dir_ax.txt +0 -459
  248. scitex/diagram/_compile.py +0 -312
  249. scitex/diagram/_diagram.py +0 -355
  250. scitex/diagram/_mcp/__init__.py +0 -4
  251. scitex/diagram/_mcp/handlers.py +0 -400
  252. scitex/diagram/_mcp/tool_schemas.py +0 -157
  253. scitex/diagram/_presets.py +0 -173
  254. scitex/diagram/_schema.py +0 -182
  255. scitex/diagram/_split.py +0 -278
  256. scitex/gen/_ci.py +0 -12
  257. scitex/gen/_title_case.py +0 -89
  258. scitex/plt/_mcp/__init__.py +0 -4
  259. scitex/plt/_mcp/_handlers_annotation.py +0 -102
  260. scitex/plt/_mcp/_handlers_figure.py +0 -195
  261. scitex/plt/_mcp/_handlers_plot.py +0 -252
  262. scitex/plt/_mcp/_handlers_style.py +0 -219
  263. scitex/plt/_mcp/handlers.py +0 -74
  264. scitex/plt/_mcp/tool_schemas.py +0 -497
  265. scitex/plt/mcp_server.py +0 -231
  266. scitex/scholar/data/.gitkeep +0 -0
  267. scitex/scholar/data/README.md +0 -44
  268. scitex/scholar/data/bib_files/bibliography.bib +0 -1952
  269. scitex/scholar/data/bib_files/neurovista.bib +0 -277
  270. scitex/scholar/data/bib_files/neurovista_enriched.bib +0 -441
  271. scitex/scholar/data/bib_files/neurovista_enriched_enriched.bib +0 -441
  272. scitex/scholar/data/bib_files/neurovista_processed.bib +0 -338
  273. scitex/scholar/data/bib_files/openaccess.bib +0 -89
  274. scitex/scholar/data/bib_files/pac-seizure_prediction_enriched.bib +0 -2178
  275. scitex/scholar/data/bib_files/pac.bib +0 -698
  276. scitex/scholar/data/bib_files/pac_enriched.bib +0 -1061
  277. scitex/scholar/data/bib_files/pac_processed.bib +0 -0
  278. scitex/scholar/data/bib_files/pac_titles.txt +0 -75
  279. scitex/scholar/data/bib_files/paywalled.bib +0 -98
  280. scitex/scholar/data/bib_files/related-papers-by-coauthors.bib +0 -58
  281. scitex/scholar/data/bib_files/related-papers-by-coauthors_enriched.bib +0 -87
  282. scitex/scholar/data/bib_files/seizure_prediction.bib +0 -694
  283. scitex/scholar/data/bib_files/seizure_prediction_processed.bib +0 -0
  284. scitex/scholar/data/bib_files/test_complete_enriched.bib +0 -437
  285. scitex/scholar/data/bib_files/test_final_enriched.bib +0 -437
  286. scitex/scholar/data/bib_files/test_seizure.bib +0 -46
  287. scitex/scholar/data/impact_factor/JCR_IF_2022.xlsx +0 -0
  288. scitex/scholar/data/impact_factor/JCR_IF_2024.db +0 -0
  289. scitex/scholar/data/impact_factor/JCR_IF_2024.xlsx +0 -0
  290. scitex/scholar/data/impact_factor/JCR_IF_2024_v01.db +0 -0
  291. scitex/scholar/data/impact_factor.db +0 -0
  292. scitex/scholar/examples/SUGGESTIONS.md +0 -865
  293. scitex/scholar/examples/dev.py +0 -38
  294. scitex-2.14.0.dist-info/METADATA +0 -1238
  295. /scitex/{gen → context}/_detect_environment.py +0 -0
  296. /scitex/{gen → context}/_get_notebook_path.py +0 -0
  297. /scitex/{gen/_shell.py → sh/_shell_legacy.py} +0 -0
  298. {scitex-2.14.0.dist-info → scitex-2.15.2.dist-info}/WHEEL +0 -0
  299. {scitex-2.14.0.dist-info → scitex-2.15.2.dist-info}/entry_points.txt +0 -0
  300. {scitex-2.14.0.dist-info → scitex-2.15.2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,187 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: "2026-01-24 (ywatanabe)"
3
+ # File: /home/ywatanabe/proj/scitex-python/src/scitex/canvas/editor/_dearpygui/_plotting.py
4
+
5
+ """
6
+ CSV data plotting for DearPyGui editor.
7
+
8
+ Handles reconstructing plots from CSV data with trace info.
9
+ """
10
+
11
+ from typing import Any, Dict, Optional
12
+
13
+ import pandas as pd
14
+
15
+
16
+ def plot_from_csv(
17
+ ax,
18
+ overrides: Dict[str, Any],
19
+ csv_data: pd.DataFrame,
20
+ highlight_trace: Optional[int] = None,
21
+ hover_trace: Optional[int] = None,
22
+ ) -> None:
23
+ """Reconstruct plot from CSV data using trace info.
24
+
25
+ Parameters
26
+ ----------
27
+ ax : matplotlib.axes.Axes
28
+ The axes to plot on
29
+ overrides : dict
30
+ Current overrides containing trace info
31
+ csv_data : pd.DataFrame
32
+ CSV data to plot
33
+ highlight_trace : int, optional
34
+ Index of trace to highlight with selection effect (yellow glow)
35
+ hover_trace : int, optional
36
+ Index of trace to highlight with hover effect (cyan glow)
37
+ """
38
+ from .._defaults import _normalize_legend_loc
39
+
40
+ if not isinstance(csv_data, pd.DataFrame):
41
+ return
42
+
43
+ df = csv_data
44
+ linewidth = overrides.get("linewidth", 1.0)
45
+ legend_visible = overrides.get("legend_visible", True)
46
+ legend_fontsize = overrides.get("legend_fontsize", 6)
47
+ legend_frameon = overrides.get("legend_frameon", False)
48
+ legend_loc = _normalize_legend_loc(overrides.get("legend_loc", "best"))
49
+
50
+ traces = overrides.get("traces", [])
51
+
52
+ if traces:
53
+ _plot_with_traces(
54
+ ax,
55
+ df,
56
+ traces,
57
+ linewidth,
58
+ highlight_trace,
59
+ hover_trace,
60
+ legend_visible,
61
+ legend_fontsize,
62
+ legend_frameon,
63
+ legend_loc,
64
+ )
65
+ else:
66
+ _plot_fallback(
67
+ ax,
68
+ df,
69
+ linewidth,
70
+ legend_visible,
71
+ legend_fontsize,
72
+ legend_frameon,
73
+ legend_loc,
74
+ )
75
+
76
+
77
+ def _plot_with_traces(
78
+ ax,
79
+ df: pd.DataFrame,
80
+ traces: list,
81
+ linewidth: float,
82
+ highlight_trace: Optional[int],
83
+ hover_trace: Optional[int],
84
+ legend_visible: bool,
85
+ legend_fontsize: int,
86
+ legend_frameon: bool,
87
+ legend_loc: str,
88
+ ) -> None:
89
+ """Plot using trace definitions."""
90
+ for i, trace in enumerate(traces):
91
+ csv_cols = trace.get("csv_columns", {})
92
+ x_col = csv_cols.get("x")
93
+ y_col = csv_cols.get("y")
94
+
95
+ if x_col in df.columns and y_col in df.columns:
96
+ trace_linewidth = trace.get("linewidth", linewidth)
97
+ is_selected = highlight_trace is not None and i == highlight_trace
98
+ is_hovered = (
99
+ hover_trace is not None and i == hover_trace and not is_selected
100
+ )
101
+
102
+ # Draw selection glow (yellow, stronger)
103
+ if is_selected:
104
+ ax.plot(
105
+ df[x_col],
106
+ df[y_col],
107
+ color="yellow",
108
+ linewidth=trace_linewidth * 4,
109
+ alpha=0.5,
110
+ zorder=0,
111
+ )
112
+ # Draw hover glow (cyan, subtler)
113
+ elif is_hovered:
114
+ ax.plot(
115
+ df[x_col],
116
+ df[y_col],
117
+ color="cyan",
118
+ linewidth=trace_linewidth * 3,
119
+ alpha=0.3,
120
+ zorder=0,
121
+ )
122
+
123
+ ax.plot(
124
+ df[x_col],
125
+ df[y_col],
126
+ label=trace.get("label", trace.get("id", "")),
127
+ color=trace.get("color"),
128
+ linestyle=trace.get("linestyle", "-"),
129
+ linewidth=trace_linewidth
130
+ * (1.5 if is_selected else (1.2 if is_hovered else 1.0)),
131
+ marker=trace.get("marker", None),
132
+ markersize=trace.get("markersize", 6),
133
+ zorder=10 if is_selected else (5 if is_hovered else 1),
134
+ )
135
+
136
+ if legend_visible and any(t.get("label") for t in traces):
137
+ ax.legend(fontsize=legend_fontsize, frameon=legend_frameon, loc=legend_loc)
138
+
139
+
140
+ def _plot_fallback(
141
+ ax,
142
+ df: pd.DataFrame,
143
+ linewidth: float,
144
+ legend_visible: bool,
145
+ legend_fontsize: int,
146
+ legend_frameon: bool,
147
+ legend_loc: str,
148
+ ) -> None:
149
+ """Fallback plotting when no traces defined - parse column names."""
150
+ cols = df.columns.tolist()
151
+ trace_groups = {}
152
+
153
+ for col in cols:
154
+ if col.endswith("_x"):
155
+ trace_id = col[:-2]
156
+ y_col = trace_id + "_y"
157
+ if y_col in cols:
158
+ parts = trace_id.split("_")
159
+ label = parts[2] if len(parts) > 2 else trace_id
160
+ trace_groups[trace_id] = {
161
+ "x_col": col,
162
+ "y_col": y_col,
163
+ "label": label,
164
+ }
165
+
166
+ if trace_groups:
167
+ for trace_id, info in trace_groups.items():
168
+ ax.plot(
169
+ df[info["x_col"]],
170
+ df[info["y_col"]],
171
+ label=info["label"],
172
+ linewidth=linewidth,
173
+ )
174
+ if legend_visible:
175
+ ax.legend(fontsize=legend_fontsize, frameon=legend_frameon, loc=legend_loc)
176
+ elif len(cols) >= 2:
177
+ x_col = cols[0]
178
+ for y_col in cols[1:]:
179
+ try:
180
+ ax.plot(df[x_col], df[y_col], label=str(y_col), linewidth=linewidth)
181
+ except Exception:
182
+ pass
183
+ if len(cols) > 2 and legend_visible:
184
+ ax.legend(fontsize=legend_fontsize, frameon=legend_frameon, loc=legend_loc)
185
+
186
+
187
+ # EOF
@@ -0,0 +1,504 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: "2026-01-24 (ywatanabe)"
3
+ # File: /home/ywatanabe/proj/scitex-python/src/scitex/canvas/editor/_dearpygui/_rendering.py
4
+
5
+ """
6
+ Figure rendering for DearPyGui editor.
7
+
8
+ Handles matplotlib figure rendering, element highlights, and hover overlays.
9
+ """
10
+
11
+ import io
12
+ from typing import TYPE_CHECKING, List, Tuple
13
+
14
+ from ._utils import MM_TO_PT, create_checkerboard
15
+
16
+ if TYPE_CHECKING:
17
+ from ._state import EditorState
18
+
19
+
20
+ def update_preview(state: "EditorState", dpg) -> None:
21
+ """Update the figure preview (full re-render).
22
+
23
+ Parameters
24
+ ----------
25
+ state : EditorState
26
+ Editor state
27
+ dpg : module
28
+ DearPyGui module
29
+ """
30
+ try:
31
+ # Mark cache dirty and do full render
32
+ state.cache_dirty = True
33
+ img_data, width, height = render_figure(state, dpg)
34
+
35
+ # Update texture
36
+ dpg.set_value("preview_texture", img_data)
37
+
38
+ # Update status
39
+ dpg.set_value("status_text", f"Preview updated ({width}x{height})")
40
+
41
+ except Exception as e:
42
+ dpg.set_value("status_text", f"Error: {str(e)}")
43
+
44
+
45
+ def update_hover_overlay(state: "EditorState", dpg) -> None:
46
+ """Fast hover overlay update using cached base image (no matplotlib re-render).
47
+
48
+ Parameters
49
+ ----------
50
+ state : EditorState
51
+ Editor state
52
+ dpg : module
53
+ DearPyGui module
54
+ """
55
+ import numpy as np
56
+ from PIL import ImageDraw
57
+
58
+ # If no cached base, do full render
59
+ if state.cached_base_image is None:
60
+ update_preview(state, dpg)
61
+ return
62
+
63
+ try:
64
+ # Start with a copy of cached base
65
+ img = state.cached_base_image.copy()
66
+ draw = ImageDraw.Draw(img, "RGBA")
67
+
68
+ # Get hover element type
69
+ hovered_type = (
70
+ state.hovered_element.get("type") if state.hovered_element else None
71
+ )
72
+ selected_type = (
73
+ state.selected_element.get("type") if state.selected_element else None
74
+ )
75
+
76
+ # Draw hover highlight (outline only, no fill) for non-trace elements
77
+ if hovered_type and hovered_type != "trace" and hovered_type != selected_type:
78
+ bbox = state.element_bboxes.get(hovered_type)
79
+ if bbox:
80
+ x0, y0, x1, y1 = bbox
81
+ # Transparent outline only - no fill to avoid covering content
82
+ draw.rectangle(
83
+ [x0 - 2, y0 - 2, x1 + 2, y1 + 2],
84
+ fill=None,
85
+ outline=(100, 180, 255, 100),
86
+ width=1,
87
+ )
88
+
89
+ # Draw selection highlight (outline only, no fill) for non-trace elements
90
+ if selected_type and selected_type != "trace":
91
+ bbox = state.element_bboxes.get(selected_type)
92
+ if bbox:
93
+ x0, y0, x1, y1 = bbox
94
+ # Transparent outline only - no fill to avoid covering content
95
+ draw.rectangle(
96
+ [x0 - 2, y0 - 2, x1 + 2, y1 + 2],
97
+ fill=None,
98
+ outline=(255, 200, 80, 150),
99
+ width=2,
100
+ )
101
+
102
+ # Convert to DearPyGui texture format
103
+ img_array = np.array(img).astype(np.float32) / 255.0
104
+ img_data = img_array.flatten().tolist()
105
+
106
+ # Update texture
107
+ dpg.set_value("preview_texture", img_data)
108
+
109
+ except Exception:
110
+ # Fallback to full render on error
111
+ update_preview(state, dpg)
112
+
113
+
114
+ def render_figure(state: "EditorState", dpg) -> Tuple[List[float], int, int]:
115
+ """Render figure and return as RGBA data for texture.
116
+
117
+ Parameters
118
+ ----------
119
+ state : EditorState
120
+ Editor state
121
+ dpg : module
122
+ DearPyGui module
123
+
124
+ Returns
125
+ -------
126
+ tuple
127
+ (img_data, width, height)
128
+ """
129
+ import matplotlib
130
+
131
+ matplotlib.use("Agg")
132
+ import matplotlib.pyplot as plt
133
+ import numpy as np
134
+ from matplotlib.ticker import MaxNLocator
135
+ from PIL import Image
136
+
137
+ from ._plotting import plot_from_csv
138
+
139
+ o = state.current_overrides
140
+
141
+ # Dimensions - use fixed size for preview
142
+ preview_dpi = 100
143
+ fig_size = o.get("fig_size", [3.15, 2.68])
144
+
145
+ # Create figure with white background for preview
146
+ fig, ax = plt.subplots(figsize=fig_size, dpi=preview_dpi)
147
+
148
+ # For preview, use white background (transparent doesn't show well in GUI)
149
+ fig.patch.set_facecolor("white")
150
+ ax.patch.set_facecolor("white")
151
+
152
+ # Plot from CSV data (only pass selection, hover is via PIL overlay for speed)
153
+ if state.csv_data is not None:
154
+ plot_from_csv(ax, o, state.csv_data, highlight_trace=state.selected_trace_index)
155
+ else:
156
+ ax.text(
157
+ 0.5,
158
+ 0.5,
159
+ "No plot data available\n(CSV not found)",
160
+ ha="center",
161
+ va="center",
162
+ transform=ax.transAxes,
163
+ fontsize=o.get("axis_fontsize", 7),
164
+ )
165
+
166
+ # Apply labels
167
+ if o.get("title"):
168
+ ax.set_title(o["title"], fontsize=o.get("title_fontsize", 8))
169
+ if o.get("xlabel"):
170
+ ax.set_xlabel(o["xlabel"], fontsize=o.get("axis_fontsize", 7))
171
+ if o.get("ylabel"):
172
+ ax.set_ylabel(o["ylabel"], fontsize=o.get("axis_fontsize", 7))
173
+
174
+ # Tick styling
175
+ ax.tick_params(
176
+ axis="both",
177
+ labelsize=o.get("tick_fontsize", 7),
178
+ length=o.get("tick_length", 0.8) * MM_TO_PT,
179
+ width=o.get("tick_width", 0.2) * MM_TO_PT,
180
+ direction=o.get("tick_direction", "out"),
181
+ )
182
+
183
+ # Number of ticks
184
+ ax.xaxis.set_major_locator(MaxNLocator(nbins=o.get("n_ticks", 4)))
185
+ ax.yaxis.set_major_locator(MaxNLocator(nbins=o.get("n_ticks", 4)))
186
+
187
+ # Grid
188
+ if o.get("grid"):
189
+ ax.grid(True, linewidth=o.get("axis_width", 0.2) * MM_TO_PT, alpha=0.3)
190
+
191
+ # Axis limits
192
+ if o.get("xlim"):
193
+ ax.set_xlim(o["xlim"])
194
+ if o.get("ylim"):
195
+ ax.set_ylim(o["ylim"])
196
+
197
+ # Spines
198
+ if o.get("hide_top_spine", True):
199
+ ax.spines["top"].set_visible(False)
200
+ if o.get("hide_right_spine", True):
201
+ ax.spines["right"].set_visible(False)
202
+
203
+ for spine in ax.spines.values():
204
+ spine.set_linewidth(o.get("axis_width", 0.2) * MM_TO_PT)
205
+
206
+ # Annotations
207
+ for annot in o.get("annotations", []):
208
+ if annot.get("type") == "text":
209
+ ax.text(
210
+ annot.get("x", 0.5),
211
+ annot.get("y", 0.5),
212
+ annot.get("text", ""),
213
+ transform=ax.transAxes,
214
+ fontsize=annot.get("fontsize", o.get("axis_fontsize", 7)),
215
+ )
216
+
217
+ fig.tight_layout()
218
+
219
+ # Draw before collecting bboxes so we have accurate positions
220
+ fig.canvas.draw()
221
+
222
+ # Draw hover/selection highlights for non-trace elements
223
+ draw_element_highlights(state, fig, ax)
224
+
225
+ # Store axes transform info for click-to-select
226
+ fig.canvas.draw()
227
+ ax_bbox = ax.get_position()
228
+ fig_width_px = int(fig_size[0] * preview_dpi)
229
+ fig_height_px = int(fig_size[1] * preview_dpi)
230
+
231
+ # Collect element bboxes for click detection
232
+ _collect_element_bboxes(state, fig, ax)
233
+
234
+ # Convert to RGBA data for DearPyGui texture
235
+ buf = io.BytesIO()
236
+ fig.savefig(
237
+ buf,
238
+ format="png",
239
+ dpi=preview_dpi,
240
+ bbox_inches="tight",
241
+ facecolor="white",
242
+ edgecolor="none",
243
+ )
244
+ buf.seek(0)
245
+
246
+ # Load with PIL and convert to normalized RGBA
247
+ img = Image.open(buf).convert("RGBA")
248
+ width, height = img.size
249
+
250
+ # Resize to fit within max preview size while preserving aspect ratio
251
+ max_width, max_height = 800, 600
252
+ ratio = min(max_width / width, max_height / height)
253
+ new_width = int(width * ratio)
254
+ new_height = int(height * ratio)
255
+ img = img.resize((new_width, new_height), Image.LANCZOS)
256
+
257
+ # Store preview bounds for coordinate conversion (after resize)
258
+ x_offset = (max_width - new_width) // 2
259
+ y_offset = (max_height - new_height) // 2
260
+ state.preview_bounds = (x_offset, y_offset, new_width, new_height)
261
+
262
+ # Scale element bboxes to preview coordinates
263
+ _scale_element_bboxes(state, ratio, x_offset, y_offset, new_height)
264
+
265
+ # Store axes transform info (scaled to resized image)
266
+ ax_x0 = int(ax_bbox.x0 * new_width)
267
+ ax_y0 = int((1 - ax_bbox.y1) * new_height) # Flip y (0 at top)
268
+ ax_width = int(ax_bbox.width * new_width)
269
+ ax_height = int(ax_bbox.height * new_height)
270
+ xlim = ax.get_xlim()
271
+ ylim = ax.get_ylim()
272
+ state.axes_transform = (ax_x0, ax_y0, ax_width, ax_height, xlim, ylim)
273
+
274
+ # Create background - checkerboard for transparent, white otherwise
275
+ transparent = o.get("transparent", True)
276
+ if transparent:
277
+ padded = create_checkerboard(max_width, max_height, square_size=10)
278
+ else:
279
+ padded = Image.new("RGBA", (max_width, max_height), (255, 255, 255, 255))
280
+
281
+ # Paste figure centered on background
282
+ padded.paste(img, (x_offset, y_offset), img)
283
+ img = padded
284
+ width, height = max_width, max_height
285
+
286
+ # Cache the base image (without highlights) for fast hover updates
287
+ state.cached_base_image = img.copy()
288
+ state.cache_dirty = False
289
+
290
+ # Convert to normalized float array for DearPyGui
291
+ img_array = np.array(img).astype(np.float32) / 255.0
292
+ img_data = img_array.flatten().tolist()
293
+
294
+ plt.close(fig)
295
+
296
+ # Update texture data
297
+ dpg.set_value("preview_texture", img_data)
298
+
299
+ return img_data, width, height
300
+
301
+
302
+ def _collect_element_bboxes(state: "EditorState", fig, ax) -> None:
303
+ """Collect element bboxes for click detection."""
304
+ renderer = fig.canvas.get_renderer()
305
+ state.element_bboxes_raw = {}
306
+
307
+ # Title bbox
308
+ if ax.title.get_text():
309
+ try:
310
+ title_bbox = ax.title.get_window_extent(renderer)
311
+ state.element_bboxes_raw["title"] = (
312
+ title_bbox.x0,
313
+ title_bbox.y0,
314
+ title_bbox.x1,
315
+ title_bbox.y1,
316
+ )
317
+ except Exception:
318
+ pass
319
+
320
+ # X label bbox
321
+ if ax.xaxis.label.get_text():
322
+ try:
323
+ xlabel_bbox = ax.xaxis.label.get_window_extent(renderer)
324
+ state.element_bboxes_raw["xlabel"] = (
325
+ xlabel_bbox.x0,
326
+ xlabel_bbox.y0,
327
+ xlabel_bbox.x1,
328
+ xlabel_bbox.y1,
329
+ )
330
+ except Exception:
331
+ pass
332
+
333
+ # Y label bbox
334
+ if ax.yaxis.label.get_text():
335
+ try:
336
+ ylabel_bbox = ax.yaxis.label.get_window_extent(renderer)
337
+ state.element_bboxes_raw["ylabel"] = (
338
+ ylabel_bbox.x0,
339
+ ylabel_bbox.y0,
340
+ ylabel_bbox.x1,
341
+ ylabel_bbox.y1,
342
+ )
343
+ except Exception:
344
+ pass
345
+
346
+ # Legend bbox
347
+ legend = ax.get_legend()
348
+ if legend:
349
+ try:
350
+ legend_bbox = legend.get_window_extent(renderer)
351
+ state.element_bboxes_raw["legend"] = (
352
+ legend_bbox.x0,
353
+ legend_bbox.y0,
354
+ legend_bbox.x1,
355
+ legend_bbox.y1,
356
+ )
357
+ except Exception:
358
+ pass
359
+
360
+ # X axis (bottom spine area)
361
+ try:
362
+ xaxis_bbox = ax.spines["bottom"].get_window_extent(renderer)
363
+ state.element_bboxes_raw["xaxis"] = (
364
+ xaxis_bbox.x0,
365
+ xaxis_bbox.y0 - 20,
366
+ xaxis_bbox.x1,
367
+ xaxis_bbox.y1 + 10,
368
+ )
369
+ except Exception:
370
+ pass
371
+
372
+ # Y axis (left spine area)
373
+ try:
374
+ yaxis_bbox = ax.spines["left"].get_window_extent(renderer)
375
+ state.element_bboxes_raw["yaxis"] = (
376
+ yaxis_bbox.x0 - 20,
377
+ yaxis_bbox.y0,
378
+ yaxis_bbox.x1 + 10,
379
+ yaxis_bbox.y1,
380
+ )
381
+ except Exception:
382
+ pass
383
+
384
+
385
+ def _scale_element_bboxes(
386
+ state: "EditorState",
387
+ ratio: float,
388
+ x_offset: int,
389
+ y_offset: int,
390
+ new_height: int,
391
+ ) -> None:
392
+ """Scale element bboxes to preview coordinates."""
393
+ state.element_bboxes = {}
394
+ for elem_type, raw_bbox in state.element_bboxes_raw.items():
395
+ if raw_bbox is None:
396
+ continue
397
+ rx0, ry0, rx1, ry1 = raw_bbox
398
+ # Scale to resized image
399
+ sx0 = int(rx0 * ratio) + x_offset
400
+ sx1 = int(rx1 * ratio) + x_offset
401
+ # Flip Y coordinate (matplotlib origin is bottom, preview is top)
402
+ sy0 = new_height - int(ry1 * ratio) + y_offset
403
+ sy1 = new_height - int(ry0 * ratio) + y_offset
404
+ state.element_bboxes[elem_type] = (sx0, sy0, sx1, sy1)
405
+
406
+
407
+ def draw_element_highlights(state: "EditorState", fig, ax) -> None:
408
+ """Draw selection highlights for non-trace elements."""
409
+ from matplotlib.patches import FancyBboxPatch
410
+
411
+ renderer = fig.canvas.get_renderer()
412
+
413
+ selected_type = (
414
+ state.selected_element.get("type") if state.selected_element else None
415
+ )
416
+
417
+ # Skip if selecting traces (handled separately in plot_from_csv)
418
+ if selected_type == "trace":
419
+ selected_type = None
420
+
421
+ def add_highlight_box(text_obj, color, alpha, linewidth=2):
422
+ """Add highlight rectangle around a text object (outline only)."""
423
+ try:
424
+ bbox = text_obj.get_window_extent(renderer)
425
+ fig_bbox = bbox.transformed(fig.transFigure.inverted())
426
+ padding = 0.01
427
+ rect = FancyBboxPatch(
428
+ (fig_bbox.x0 - padding, fig_bbox.y0 - padding),
429
+ fig_bbox.width + 2 * padding,
430
+ fig_bbox.height + 2 * padding,
431
+ boxstyle="round,pad=0.02,rounding_size=0.01",
432
+ facecolor="none",
433
+ edgecolor=color,
434
+ alpha=0.7,
435
+ linewidth=linewidth,
436
+ transform=fig.transFigure,
437
+ zorder=100,
438
+ )
439
+ fig.patches.append(rect)
440
+ except Exception:
441
+ pass
442
+
443
+ def add_spine_highlight(spine, color, alpha, linewidth=2):
444
+ """Add highlight to a spine/axis (outline only)."""
445
+ try:
446
+ bbox = spine.get_window_extent(renderer)
447
+ fig_bbox = bbox.transformed(fig.transFigure.inverted())
448
+ padding = 0.01
449
+ rect = FancyBboxPatch(
450
+ (fig_bbox.x0 - padding, fig_bbox.y0 - padding),
451
+ fig_bbox.width + 2 * padding,
452
+ fig_bbox.height + 2 * padding,
453
+ boxstyle="round,pad=0.01",
454
+ facecolor="none",
455
+ edgecolor=color,
456
+ alpha=0.7,
457
+ linewidth=linewidth,
458
+ transform=fig.transFigure,
459
+ zorder=100,
460
+ )
461
+ fig.patches.append(rect)
462
+ except Exception:
463
+ pass
464
+
465
+ # Map element types to matplotlib objects
466
+ element_map = {
467
+ "title": ax.title,
468
+ "xlabel": ax.xaxis.label,
469
+ "ylabel": ax.yaxis.label,
470
+ }
471
+
472
+ # Draw selection highlight (outline only, no fill)
473
+ select_color = "#FFC850"
474
+ if selected_type in element_map:
475
+ add_highlight_box(element_map[selected_type], select_color, 0.0, linewidth=2)
476
+ elif selected_type == "xaxis":
477
+ add_spine_highlight(ax.spines["bottom"], select_color, 0.0, linewidth=2)
478
+ elif selected_type == "yaxis":
479
+ add_spine_highlight(ax.spines["left"], select_color, 0.0, linewidth=2)
480
+ elif selected_type == "legend":
481
+ legend = ax.get_legend()
482
+ if legend:
483
+ try:
484
+ bbox = legend.get_window_extent(renderer)
485
+ fig_bbox = bbox.transformed(fig.transFigure.inverted())
486
+ padding = 0.01
487
+ rect = FancyBboxPatch(
488
+ (fig_bbox.x0 - padding, fig_bbox.y0 - padding),
489
+ fig_bbox.width + 2 * padding,
490
+ fig_bbox.height + 2 * padding,
491
+ boxstyle="round,pad=0.02",
492
+ facecolor="none",
493
+ edgecolor=select_color,
494
+ alpha=0.7,
495
+ linewidth=2,
496
+ transform=fig.transFigure,
497
+ zorder=100,
498
+ )
499
+ fig.patches.append(rect)
500
+ except Exception:
501
+ pass
502
+
503
+
504
+ # EOF