scitex 2.14.0__py3-none-any.whl → 2.15.1__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 (218) hide show
  1. scitex/__init__.py +47 -0
  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 +191 -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/writer.py +21 -204
  17. scitex/ai/_gen_ai/_PARAMS.py +10 -7
  18. scitex/ai/classification/reporters/_SingleClassificationReporter.py +45 -1603
  19. scitex/ai/classification/reporters/_mixins/__init__.py +36 -0
  20. scitex/ai/classification/reporters/_mixins/_constants.py +67 -0
  21. scitex/ai/classification/reporters/_mixins/_cv_summary.py +387 -0
  22. scitex/ai/classification/reporters/_mixins/_feature_importance.py +119 -0
  23. scitex/ai/classification/reporters/_mixins/_metrics.py +275 -0
  24. scitex/ai/classification/reporters/_mixins/_plotting.py +179 -0
  25. scitex/ai/classification/reporters/_mixins/_reports.py +153 -0
  26. scitex/ai/classification/reporters/_mixins/_storage.py +160 -0
  27. scitex/audio/README.md +40 -36
  28. scitex/audio/__init__.py +127 -59
  29. scitex/audio/_branding.py +185 -0
  30. scitex/audio/_mcp/__init__.py +32 -0
  31. scitex/audio/_mcp/handlers.py +59 -6
  32. scitex/audio/_mcp/speak_handlers.py +238 -0
  33. scitex/audio/_relay.py +225 -0
  34. scitex/audio/engines/elevenlabs_engine.py +6 -1
  35. scitex/audio/mcp_server.py +228 -75
  36. scitex/canvas/README.md +1 -1
  37. scitex/canvas/editor/_dearpygui/__init__.py +25 -0
  38. scitex/canvas/editor/_dearpygui/_editor.py +147 -0
  39. scitex/canvas/editor/_dearpygui/_handlers.py +476 -0
  40. scitex/canvas/editor/_dearpygui/_panels/__init__.py +17 -0
  41. scitex/canvas/editor/_dearpygui/_panels/_control.py +119 -0
  42. scitex/canvas/editor/_dearpygui/_panels/_element_controls.py +190 -0
  43. scitex/canvas/editor/_dearpygui/_panels/_preview.py +43 -0
  44. scitex/canvas/editor/_dearpygui/_panels/_sections.py +390 -0
  45. scitex/canvas/editor/_dearpygui/_plotting.py +187 -0
  46. scitex/canvas/editor/_dearpygui/_rendering.py +504 -0
  47. scitex/canvas/editor/_dearpygui/_selection.py +295 -0
  48. scitex/canvas/editor/_dearpygui/_state.py +93 -0
  49. scitex/canvas/editor/_dearpygui/_utils.py +61 -0
  50. scitex/canvas/editor/flask_editor/templates/__init__.py +32 -70
  51. scitex/cli/__init__.py +38 -43
  52. scitex/cli/audio.py +76 -27
  53. scitex/cli/capture.py +13 -20
  54. scitex/cli/introspect.py +443 -0
  55. scitex/cli/main.py +198 -109
  56. scitex/cli/mcp.py +60 -34
  57. scitex/cli/scholar/__init__.py +8 -0
  58. scitex/cli/scholar/_crossref_scitex.py +296 -0
  59. scitex/cli/scholar/_fetch.py +25 -3
  60. scitex/cli/social.py +314 -0
  61. scitex/cli/writer.py +117 -0
  62. scitex/config/README.md +1 -1
  63. scitex/config/__init__.py +16 -2
  64. scitex/config/_env_registry.py +191 -0
  65. scitex/diagram/__init__.py +42 -19
  66. scitex/diagram/mcp_server.py +13 -125
  67. scitex/introspect/__init__.py +75 -0
  68. scitex/introspect/_call_graph.py +303 -0
  69. scitex/introspect/_class_hierarchy.py +163 -0
  70. scitex/introspect/_core.py +42 -0
  71. scitex/introspect/_docstring.py +131 -0
  72. scitex/introspect/_examples.py +113 -0
  73. scitex/introspect/_imports.py +271 -0
  74. scitex/introspect/_mcp/__init__.py +37 -0
  75. scitex/introspect/_mcp/handlers.py +208 -0
  76. scitex/introspect/_members.py +151 -0
  77. scitex/introspect/_resolve.py +89 -0
  78. scitex/introspect/_signature.py +131 -0
  79. scitex/introspect/_source.py +80 -0
  80. scitex/introspect/_type_hints.py +172 -0
  81. scitex/io/bundle/README.md +1 -1
  82. scitex/mcp_server.py +98 -5
  83. scitex/plt/__init__.py +248 -550
  84. scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin/_wrappers.py +5 -10
  85. scitex/plt/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
  86. scitex/plt/gallery/README.md +1 -1
  87. scitex/plt/utils/_hitmap/__init__.py +82 -0
  88. scitex/plt/utils/_hitmap/_artist_extraction.py +343 -0
  89. scitex/plt/utils/_hitmap/_color_application.py +346 -0
  90. scitex/plt/utils/_hitmap/_color_conversion.py +121 -0
  91. scitex/plt/utils/_hitmap/_constants.py +40 -0
  92. scitex/plt/utils/_hitmap/_hitmap_core.py +334 -0
  93. scitex/plt/utils/_hitmap/_path_extraction.py +357 -0
  94. scitex/plt/utils/_hitmap/_query.py +113 -0
  95. scitex/plt/utils/_hitmap.py +46 -1616
  96. scitex/plt/utils/_metadata/__init__.py +80 -0
  97. scitex/plt/utils/_metadata/_artists/__init__.py +25 -0
  98. scitex/plt/utils/_metadata/_artists/_base.py +195 -0
  99. scitex/plt/utils/_metadata/_artists/_collections.py +356 -0
  100. scitex/plt/utils/_metadata/_artists/_extract.py +57 -0
  101. scitex/plt/utils/_metadata/_artists/_images.py +80 -0
  102. scitex/plt/utils/_metadata/_artists/_lines.py +261 -0
  103. scitex/plt/utils/_metadata/_artists/_patches.py +247 -0
  104. scitex/plt/utils/_metadata/_artists/_text.py +106 -0
  105. scitex/plt/utils/_metadata/_csv.py +416 -0
  106. scitex/plt/utils/_metadata/_detect.py +225 -0
  107. scitex/plt/utils/_metadata/_legend.py +127 -0
  108. scitex/plt/utils/_metadata/_rounding.py +117 -0
  109. scitex/plt/utils/_metadata/_verification.py +202 -0
  110. scitex/schema/README.md +1 -1
  111. scitex/scholar/__init__.py +8 -0
  112. scitex/scholar/_mcp/crossref_handlers.py +265 -0
  113. scitex/scholar/core/Scholar.py +63 -1700
  114. scitex/scholar/core/_mixins/__init__.py +36 -0
  115. scitex/scholar/core/_mixins/_enrichers.py +270 -0
  116. scitex/scholar/core/_mixins/_library_handlers.py +100 -0
  117. scitex/scholar/core/_mixins/_loaders.py +103 -0
  118. scitex/scholar/core/_mixins/_pdf_download.py +375 -0
  119. scitex/scholar/core/_mixins/_pipeline.py +312 -0
  120. scitex/scholar/core/_mixins/_project_handlers.py +125 -0
  121. scitex/scholar/core/_mixins/_savers.py +69 -0
  122. scitex/scholar/core/_mixins/_search.py +103 -0
  123. scitex/scholar/core/_mixins/_services.py +88 -0
  124. scitex/scholar/core/_mixins/_url_finding.py +105 -0
  125. scitex/scholar/crossref_scitex.py +367 -0
  126. scitex/scholar/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
  127. scitex/scholar/examples/00_run_all.sh +120 -0
  128. scitex/scholar/jobs/_executors.py +27 -3
  129. scitex/scholar/pdf_download/ScholarPDFDownloader.py +38 -416
  130. scitex/scholar/pdf_download/_cli.py +154 -0
  131. scitex/scholar/pdf_download/strategies/__init__.py +11 -8
  132. scitex/scholar/pdf_download/strategies/manual_download_fallback.py +80 -3
  133. scitex/scholar/pipelines/ScholarPipelineBibTeX.py +73 -121
  134. scitex/scholar/pipelines/ScholarPipelineParallel.py +80 -138
  135. scitex/scholar/pipelines/ScholarPipelineSingle.py +43 -63
  136. scitex/scholar/pipelines/_single_steps.py +71 -36
  137. scitex/scholar/storage/_LibraryManager.py +97 -1695
  138. scitex/scholar/storage/_mixins/__init__.py +30 -0
  139. scitex/scholar/storage/_mixins/_bibtex_handlers.py +128 -0
  140. scitex/scholar/storage/_mixins/_library_operations.py +218 -0
  141. scitex/scholar/storage/_mixins/_metadata_conversion.py +226 -0
  142. scitex/scholar/storage/_mixins/_paper_saving.py +456 -0
  143. scitex/scholar/storage/_mixins/_resolution.py +376 -0
  144. scitex/scholar/storage/_mixins/_storage_helpers.py +121 -0
  145. scitex/scholar/storage/_mixins/_symlink_handlers.py +226 -0
  146. scitex/scholar/url_finder/.tmp/open_url/KNOWN_RESOLVERS.py +462 -0
  147. scitex/scholar/url_finder/.tmp/open_url/README.md +223 -0
  148. scitex/scholar/url_finder/.tmp/open_url/_DOIToURLResolver.py +694 -0
  149. scitex/scholar/url_finder/.tmp/open_url/_OpenURLResolver.py +1160 -0
  150. scitex/scholar/url_finder/.tmp/open_url/_ResolverLinkFinder.py +344 -0
  151. scitex/scholar/url_finder/.tmp/open_url/__init__.py +24 -0
  152. scitex/security/README.md +3 -3
  153. scitex/session/README.md +1 -1
  154. scitex/sh/README.md +1 -1
  155. scitex/social/__init__.py +153 -0
  156. scitex/social/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
  157. scitex/template/README.md +1 -1
  158. scitex/template/clone_writer_directory.py +5 -5
  159. scitex/writer/README.md +1 -1
  160. scitex/writer/_mcp/handlers.py +11 -744
  161. scitex/writer/_mcp/tool_schemas.py +5 -335
  162. scitex-2.15.1.dist-info/METADATA +648 -0
  163. {scitex-2.14.0.dist-info → scitex-2.15.1.dist-info}/RECORD +166 -111
  164. scitex/canvas/editor/flask_editor/templates/_scripts.py +0 -4933
  165. scitex/canvas/editor/flask_editor/templates/_styles.py +0 -1658
  166. scitex/dev/plt/data/mpl/PLOTTING_FUNCTIONS.yaml +0 -90
  167. scitex/dev/plt/data/mpl/PLOTTING_SIGNATURES.yaml +0 -1571
  168. scitex/dev/plt/data/mpl/PLOTTING_SIGNATURES_DETAILED.yaml +0 -6262
  169. scitex/dev/plt/data/mpl/SIGNATURES_FLATTENED.yaml +0 -1274
  170. scitex/dev/plt/data/mpl/dir_ax.txt +0 -459
  171. scitex/diagram/_compile.py +0 -312
  172. scitex/diagram/_diagram.py +0 -355
  173. scitex/diagram/_mcp/__init__.py +0 -4
  174. scitex/diagram/_mcp/handlers.py +0 -400
  175. scitex/diagram/_mcp/tool_schemas.py +0 -157
  176. scitex/diagram/_presets.py +0 -173
  177. scitex/diagram/_schema.py +0 -182
  178. scitex/diagram/_split.py +0 -278
  179. scitex/plt/_mcp/__init__.py +0 -4
  180. scitex/plt/_mcp/_handlers_annotation.py +0 -102
  181. scitex/plt/_mcp/_handlers_figure.py +0 -195
  182. scitex/plt/_mcp/_handlers_plot.py +0 -252
  183. scitex/plt/_mcp/_handlers_style.py +0 -219
  184. scitex/plt/_mcp/handlers.py +0 -74
  185. scitex/plt/_mcp/tool_schemas.py +0 -497
  186. scitex/plt/mcp_server.py +0 -231
  187. scitex/scholar/data/.gitkeep +0 -0
  188. scitex/scholar/data/README.md +0 -44
  189. scitex/scholar/data/bib_files/bibliography.bib +0 -1952
  190. scitex/scholar/data/bib_files/neurovista.bib +0 -277
  191. scitex/scholar/data/bib_files/neurovista_enriched.bib +0 -441
  192. scitex/scholar/data/bib_files/neurovista_enriched_enriched.bib +0 -441
  193. scitex/scholar/data/bib_files/neurovista_processed.bib +0 -338
  194. scitex/scholar/data/bib_files/openaccess.bib +0 -89
  195. scitex/scholar/data/bib_files/pac-seizure_prediction_enriched.bib +0 -2178
  196. scitex/scholar/data/bib_files/pac.bib +0 -698
  197. scitex/scholar/data/bib_files/pac_enriched.bib +0 -1061
  198. scitex/scholar/data/bib_files/pac_processed.bib +0 -0
  199. scitex/scholar/data/bib_files/pac_titles.txt +0 -75
  200. scitex/scholar/data/bib_files/paywalled.bib +0 -98
  201. scitex/scholar/data/bib_files/related-papers-by-coauthors.bib +0 -58
  202. scitex/scholar/data/bib_files/related-papers-by-coauthors_enriched.bib +0 -87
  203. scitex/scholar/data/bib_files/seizure_prediction.bib +0 -694
  204. scitex/scholar/data/bib_files/seizure_prediction_processed.bib +0 -0
  205. scitex/scholar/data/bib_files/test_complete_enriched.bib +0 -437
  206. scitex/scholar/data/bib_files/test_final_enriched.bib +0 -437
  207. scitex/scholar/data/bib_files/test_seizure.bib +0 -46
  208. scitex/scholar/data/impact_factor/JCR_IF_2022.xlsx +0 -0
  209. scitex/scholar/data/impact_factor/JCR_IF_2024.db +0 -0
  210. scitex/scholar/data/impact_factor/JCR_IF_2024.xlsx +0 -0
  211. scitex/scholar/data/impact_factor/JCR_IF_2024_v01.db +0 -0
  212. scitex/scholar/data/impact_factor.db +0 -0
  213. scitex/scholar/examples/SUGGESTIONS.md +0 -865
  214. scitex/scholar/examples/dev.py +0 -38
  215. scitex-2.14.0.dist-info/METADATA +0 -1238
  216. {scitex-2.14.0.dist-info → scitex-2.15.1.dist-info}/WHEEL +0 -0
  217. {scitex-2.14.0.dist-info → scitex-2.15.1.dist-info}/entry_points.txt +0 -0
  218. {scitex-2.14.0.dist-info → scitex-2.15.1.dist-info}/licenses/LICENSE +0 -0
@@ -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