scitex 2.14.0__py3-none-any.whl → 2.15.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (264) 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 +27 -0
  16. scitex/_mcp_tools/template.py +24 -0
  17. scitex/_mcp_tools/writer.py +17 -210
  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 +160 -41
  68. scitex/cli/capture.py +133 -20
  69. scitex/cli/introspect.py +488 -0
  70. scitex/cli/main.py +200 -109
  71. scitex/cli/mcp.py +60 -34
  72. scitex/cli/plt.py +414 -0
  73. scitex/cli/repro.py +15 -8
  74. scitex/cli/resource.py +15 -8
  75. scitex/cli/scholar/__init__.py +154 -8
  76. scitex/cli/scholar/_crossref_scitex.py +296 -0
  77. scitex/cli/scholar/_fetch.py +25 -3
  78. scitex/cli/social.py +355 -0
  79. scitex/cli/stats.py +136 -11
  80. scitex/cli/template.py +129 -12
  81. scitex/cli/tex.py +15 -8
  82. scitex/cli/writer.py +49 -299
  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} +48 -56
  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/security/README.md +3 -3
  178. scitex/session/README.md +1 -1
  179. scitex/session/__init__.py +26 -7
  180. scitex/session/_decorator.py +1 -1
  181. scitex/sh/README.md +1 -1
  182. scitex/sh/__init__.py +7 -4
  183. scitex/social/__init__.py +155 -0
  184. scitex/social/docs/EXTERNAL_PACKAGE_BRANDING.md +149 -0
  185. scitex/stats/_mcp/_handlers/__init__.py +31 -0
  186. scitex/stats/_mcp/_handlers/_corrections.py +113 -0
  187. scitex/stats/_mcp/_handlers/_descriptive.py +78 -0
  188. scitex/stats/_mcp/_handlers/_effect_size.py +106 -0
  189. scitex/stats/_mcp/_handlers/_format.py +94 -0
  190. scitex/stats/_mcp/_handlers/_normality.py +110 -0
  191. scitex/stats/_mcp/_handlers/_posthoc.py +224 -0
  192. scitex/stats/_mcp/_handlers/_power.py +247 -0
  193. scitex/stats/_mcp/_handlers/_recommend.py +102 -0
  194. scitex/stats/_mcp/_handlers/_run_test.py +279 -0
  195. scitex/stats/_mcp/_handlers/_stars.py +48 -0
  196. scitex/stats/_mcp/handlers.py +19 -1171
  197. scitex/stats/auto/_stat_style.py +175 -0
  198. scitex/stats/auto/_style_definitions.py +411 -0
  199. scitex/stats/auto/_styles.py +22 -620
  200. scitex/stats/descriptive/__init__.py +11 -8
  201. scitex/stats/descriptive/_ci.py +39 -0
  202. scitex/stats/power/_power.py +15 -4
  203. scitex/str/__init__.py +2 -1
  204. scitex/str/_title_case.py +63 -0
  205. scitex/template/README.md +1 -1
  206. scitex/template/__init__.py +25 -10
  207. scitex/template/_code_templates.py +147 -0
  208. scitex/template/_mcp/handlers.py +81 -0
  209. scitex/template/_mcp/tool_schemas.py +55 -0
  210. scitex/template/_templates/__init__.py +51 -0
  211. scitex/template/_templates/audio.py +233 -0
  212. scitex/template/_templates/canvas.py +312 -0
  213. scitex/template/_templates/capture.py +268 -0
  214. scitex/template/_templates/config.py +43 -0
  215. scitex/template/_templates/diagram.py +294 -0
  216. scitex/template/_templates/io.py +107 -0
  217. scitex/template/_templates/module.py +53 -0
  218. scitex/template/_templates/plt.py +202 -0
  219. scitex/template/_templates/scholar.py +267 -0
  220. scitex/template/_templates/session.py +130 -0
  221. scitex/template/_templates/session_minimal.py +43 -0
  222. scitex/template/_templates/session_plot.py +67 -0
  223. scitex/template/_templates/session_stats.py +77 -0
  224. scitex/template/_templates/stats.py +323 -0
  225. scitex/template/_templates/writer.py +296 -0
  226. scitex/template/clone_writer_directory.py +5 -5
  227. scitex/ui/_backends/_email.py +10 -2
  228. scitex/ui/_backends/_webhook.py +5 -1
  229. scitex/web/_search_pubmed.py +10 -6
  230. scitex/writer/README.md +1 -1
  231. scitex/writer/__init__.py +43 -34
  232. scitex/writer/_mcp/handlers.py +11 -744
  233. scitex/writer/_mcp/tool_schemas.py +5 -335
  234. scitex-2.15.3.dist-info/METADATA +667 -0
  235. {scitex-2.14.0.dist-info → scitex-2.15.3.dist-info}/RECORD +241 -120
  236. scitex/canvas/editor/flask_editor/templates/_scripts.py +0 -4933
  237. scitex/canvas/editor/flask_editor/templates/_styles.py +0 -1658
  238. scitex/diagram/_compile.py +0 -312
  239. scitex/diagram/_diagram.py +0 -355
  240. scitex/diagram/_mcp/__init__.py +0 -4
  241. scitex/diagram/_mcp/handlers.py +0 -400
  242. scitex/diagram/_mcp/tool_schemas.py +0 -157
  243. scitex/diagram/_presets.py +0 -173
  244. scitex/diagram/_schema.py +0 -182
  245. scitex/diagram/_split.py +0 -278
  246. scitex/gen/_ci.py +0 -12
  247. scitex/gen/_title_case.py +0 -89
  248. scitex/plt/_mcp/__init__.py +0 -4
  249. scitex/plt/_mcp/_handlers_annotation.py +0 -102
  250. scitex/plt/_mcp/_handlers_figure.py +0 -195
  251. scitex/plt/_mcp/_handlers_plot.py +0 -252
  252. scitex/plt/_mcp/_handlers_style.py +0 -219
  253. scitex/plt/_mcp/handlers.py +0 -74
  254. scitex/plt/_mcp/tool_schemas.py +0 -497
  255. scitex/plt/mcp_server.py +0 -231
  256. scitex/scholar/examples/SUGGESTIONS.md +0 -865
  257. scitex/scholar/examples/dev.py +0 -38
  258. scitex-2.14.0.dist-info/METADATA +0 -1238
  259. /scitex/{gen → context}/_detect_environment.py +0 -0
  260. /scitex/{gen → context}/_get_notebook_path.py +0 -0
  261. /scitex/{gen/_shell.py → sh/_shell_legacy.py} +0 -0
  262. {scitex-2.14.0.dist-info → scitex-2.15.3.dist-info}/WHEEL +0 -0
  263. {scitex-2.14.0.dist-info → scitex-2.15.3.dist-info}/entry_points.txt +0 -0
  264. {scitex-2.14.0.dist-info → scitex-2.15.3.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,295 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: "2026-01-24 (ywatanabe)"
3
+ # File: /home/ywatanabe/proj/scitex-python/src/scitex/canvas/editor/_dearpygui/_selection.py
4
+
5
+ """
6
+ Element selection for DearPyGui editor.
7
+
8
+ Handles click-to-select, hover detection, and element finding.
9
+ """
10
+
11
+ from typing import TYPE_CHECKING, Dict, List, Optional
12
+
13
+ import numpy as np
14
+ import pandas as pd
15
+
16
+ if TYPE_CHECKING:
17
+ from ._state import EditorState
18
+
19
+
20
+ def get_trace_labels(state: "EditorState") -> List[str]:
21
+ """Get list of trace labels for selection combo."""
22
+ traces = state.current_overrides.get("traces", [])
23
+ if not traces:
24
+ return ["(no traces)"]
25
+ return [t.get("label", t.get("id", f"Trace {i}")) for i, t in enumerate(traces)]
26
+
27
+
28
+ def get_all_element_labels(state: "EditorState") -> List[str]:
29
+ """Get list of all selectable element labels."""
30
+ labels = []
31
+
32
+ # Fixed elements
33
+ labels.append("Title")
34
+ labels.append("X Label")
35
+ labels.append("Y Label")
36
+ labels.append("X Axis")
37
+ labels.append("Y Axis")
38
+ labels.append("Legend")
39
+
40
+ # Traces
41
+ traces = state.current_overrides.get("traces", [])
42
+ for i, t in enumerate(traces):
43
+ label = t.get("label", t.get("id", f"Trace {i}"))
44
+ labels.append(f"Trace: {label}")
45
+
46
+ return labels
47
+
48
+
49
+ def find_clicked_element(
50
+ state: "EditorState", click_x: float, click_y: float
51
+ ) -> Optional[Dict]:
52
+ """Find which element was clicked based on stored bboxes."""
53
+ if not state.element_bboxes:
54
+ return None
55
+
56
+ # Check each element bbox
57
+ for element_type, bbox in state.element_bboxes.items():
58
+ if bbox is None:
59
+ continue
60
+ x0, y0, x1, y1 = bbox
61
+ if x0 <= click_x <= x1 and y0 <= click_y <= y1:
62
+ return {"type": element_type, "index": None}
63
+
64
+ return None
65
+
66
+
67
+ def find_nearest_trace(
68
+ state: "EditorState",
69
+ click_x: float,
70
+ click_y: float,
71
+ preview_width: int,
72
+ preview_height: int,
73
+ ) -> Optional[int]:
74
+ """Find the nearest trace to the click position."""
75
+ if state.csv_data is None or not isinstance(state.csv_data, pd.DataFrame):
76
+ return None
77
+
78
+ traces = state.current_overrides.get("traces", [])
79
+ if not traces:
80
+ return None
81
+
82
+ # Get preview bounds from last render
83
+ if state.preview_bounds is None:
84
+ return None
85
+
86
+ x_offset, y_offset, fig_width, fig_height = state.preview_bounds
87
+
88
+ # Adjust click coordinates to figure space
89
+ fig_x = click_x - x_offset
90
+ fig_y = click_y - y_offset
91
+
92
+ # Check if click is within figure bounds
93
+ if not (0 <= fig_x <= fig_width and 0 <= fig_y <= fig_height):
94
+ return None
95
+
96
+ # Get axes transform info
97
+ if state.axes_transform is None:
98
+ return None
99
+
100
+ ax_x0, ax_y0, ax_width, ax_height, xlim, ylim = state.axes_transform
101
+
102
+ # Convert figure pixel to axes pixel
103
+ ax_pixel_x = fig_x - ax_x0
104
+ ax_pixel_y = fig_y - ax_y0
105
+
106
+ # Check if click is within axes bounds
107
+ if not (0 <= ax_pixel_x <= ax_width and 0 <= ax_pixel_y <= ax_height):
108
+ return None
109
+
110
+ # Convert axes pixel to data coordinates
111
+ # Note: y is flipped (0 at top in pixel space)
112
+ data_x = xlim[0] + (ax_pixel_x / ax_width) * (xlim[1] - xlim[0])
113
+ data_y = ylim[1] - (ax_pixel_y / ax_height) * (ylim[1] - ylim[0])
114
+
115
+ # Find nearest trace
116
+ df = state.csv_data
117
+ min_dist = float("inf")
118
+ nearest_idx = None
119
+
120
+ for i, trace in enumerate(traces):
121
+ csv_cols = trace.get("csv_columns", {})
122
+ x_col = csv_cols.get("x")
123
+ y_col = csv_cols.get("y")
124
+
125
+ if x_col not in df.columns or y_col not in df.columns:
126
+ continue
127
+
128
+ trace_x = df[x_col].dropna().values
129
+ trace_y = df[y_col].dropna().values
130
+
131
+ if len(trace_x) == 0:
132
+ continue
133
+
134
+ # Normalize coordinates for distance calculation
135
+ x_range = xlim[1] - xlim[0]
136
+ y_range = ylim[1] - ylim[0]
137
+
138
+ norm_click_x = (data_x - xlim[0]) / x_range if x_range > 0 else 0
139
+ norm_click_y = (data_y - ylim[0]) / y_range if y_range > 0 else 0
140
+
141
+ norm_trace_x = (trace_x - xlim[0]) / x_range if x_range > 0 else trace_x
142
+ norm_trace_y = (trace_y - ylim[0]) / y_range if y_range > 0 else trace_y
143
+
144
+ # Calculate distances to all points
145
+ distances = np.sqrt(
146
+ (norm_trace_x - norm_click_x) ** 2 + (norm_trace_y - norm_click_y) ** 2
147
+ )
148
+ min_trace_dist = np.min(distances)
149
+
150
+ if min_trace_dist < min_dist:
151
+ min_dist = min_trace_dist
152
+ nearest_idx = i
153
+
154
+ # Only select if close enough (threshold in normalized space)
155
+ if min_dist < 0.1: # 10% of plot area
156
+ return nearest_idx
157
+
158
+ return None
159
+
160
+
161
+ def select_element(state: "EditorState", element: Dict, dpg) -> None:
162
+ """Select an element and show appropriate controls."""
163
+ from ._rendering import update_preview
164
+
165
+ state.selected_element = element
166
+ elem_type = element.get("type")
167
+ elem_idx = element.get("index")
168
+
169
+ # Hide all control groups first
170
+ dpg.configure_item("trace_controls_group", show=False)
171
+ dpg.configure_item("text_controls_group", show=False)
172
+ dpg.configure_item("axis_controls_group", show=False)
173
+ dpg.configure_item("legend_controls_group", show=False)
174
+
175
+ # Update combo selection
176
+ if elem_type == "trace":
177
+ _select_trace(state, elem_idx, dpg)
178
+ elif elem_type in ("title", "xlabel", "ylabel"):
179
+ _select_text_element(state, elem_type, dpg)
180
+ elif elem_type in ("xaxis", "yaxis"):
181
+ _select_axis_element(state, elem_type, dpg)
182
+ elif elem_type == "legend":
183
+ _select_legend(state, dpg)
184
+
185
+ # Redraw with highlight
186
+ update_preview(state, dpg)
187
+
188
+
189
+ def _select_trace(state: "EditorState", trace_idx: Optional[int], dpg) -> None:
190
+ """Handle trace selection."""
191
+ traces = state.current_overrides.get("traces", [])
192
+ if trace_idx is not None and trace_idx < len(traces):
193
+ trace = traces[trace_idx]
194
+ label = f"Trace: {trace.get('label', trace.get('id', f'Trace {trace_idx}'))}"
195
+ dpg.set_value("element_selector_combo", label)
196
+
197
+ # Show trace controls and populate
198
+ dpg.configure_item("trace_controls_group", show=True)
199
+ state.selected_trace_index = trace_idx
200
+ dpg.set_value("trace_label_input", trace.get("label", ""))
201
+
202
+ color_hex = trace.get("color", "#0080bf")
203
+ try:
204
+ r = int(color_hex[1:3], 16)
205
+ g = int(color_hex[3:5], 16)
206
+ b = int(color_hex[5:7], 16)
207
+ dpg.set_value("trace_color_picker", [r, g, b])
208
+ except (ValueError, IndexError):
209
+ dpg.set_value("trace_color_picker", [128, 128, 191])
210
+
211
+ dpg.set_value("trace_linewidth_slider", trace.get("linewidth", 1.0))
212
+ dpg.set_value("trace_linestyle_combo", trace.get("linestyle", "-"))
213
+ dpg.set_value("trace_marker_combo", trace.get("marker", "") or "")
214
+ dpg.set_value("trace_markersize_slider", trace.get("markersize", 6.0))
215
+
216
+ dpg.set_value(
217
+ "selection_text",
218
+ f"Selected: {trace.get('label', f'Trace {trace_idx}')}",
219
+ )
220
+
221
+
222
+ def _select_text_element(state: "EditorState", elem_type: str, dpg) -> None:
223
+ """Handle text element selection (title, xlabel, ylabel)."""
224
+ dpg.set_value(
225
+ "element_selector_combo",
226
+ elem_type.replace("x", "X ").replace("y", "Y ").title(),
227
+ )
228
+ dpg.configure_item("text_controls_group", show=True)
229
+
230
+ o = state.current_overrides
231
+ if elem_type == "title":
232
+ dpg.set_value("element_text_input", o.get("title", ""))
233
+ dpg.set_value("element_fontsize_slider", o.get("title_fontsize", 8))
234
+ elif elem_type == "xlabel":
235
+ dpg.set_value("element_text_input", o.get("xlabel", ""))
236
+ dpg.set_value("element_fontsize_slider", o.get("axis_fontsize", 7))
237
+ elif elem_type == "ylabel":
238
+ dpg.set_value("element_text_input", o.get("ylabel", ""))
239
+ dpg.set_value("element_fontsize_slider", o.get("axis_fontsize", 7))
240
+
241
+ dpg.set_value("selection_text", f"Selected: {elem_type.title()}")
242
+
243
+
244
+ def _select_axis_element(state: "EditorState", elem_type: str, dpg) -> None:
245
+ """Handle axis element selection (xaxis, yaxis)."""
246
+ label = "X Axis" if elem_type == "xaxis" else "Y Axis"
247
+ dpg.set_value("element_selector_combo", label)
248
+ dpg.configure_item("axis_controls_group", show=True)
249
+
250
+ o = state.current_overrides
251
+ dpg.set_value("axis_linewidth_slider", o.get("axis_width", 0.2))
252
+ dpg.set_value("axis_tick_length_slider", o.get("tick_length", 0.8))
253
+ dpg.set_value("axis_tick_fontsize_slider", o.get("tick_fontsize", 7))
254
+
255
+ if elem_type == "xaxis":
256
+ dpg.set_value("axis_show_spine_checkbox", not o.get("hide_bottom_spine", False))
257
+ else:
258
+ dpg.set_value("axis_show_spine_checkbox", not o.get("hide_left_spine", False))
259
+
260
+ dpg.set_value("selection_text", f"Selected: {label}")
261
+
262
+
263
+ def _select_legend(state: "EditorState", dpg) -> None:
264
+ """Handle legend selection."""
265
+ dpg.set_value("element_selector_combo", "Legend")
266
+ dpg.configure_item("legend_controls_group", show=True)
267
+
268
+ o = state.current_overrides
269
+ dpg.set_value("legend_visible_edit", o.get("legend_visible", True))
270
+ dpg.set_value("legend_frameon_edit", o.get("legend_frameon", False))
271
+ dpg.set_value("legend_loc_edit", o.get("legend_loc", "best"))
272
+ dpg.set_value("legend_fontsize_edit", o.get("legend_fontsize", 6))
273
+
274
+ dpg.set_value("selection_text", "Selected: Legend")
275
+
276
+
277
+ def deselect_element(state: "EditorState", dpg) -> None:
278
+ """Deselect the current element."""
279
+ from ._rendering import update_preview
280
+
281
+ state.selected_element = None
282
+ state.selected_trace_index = None
283
+
284
+ # Hide all control groups
285
+ dpg.configure_item("trace_controls_group", show=False)
286
+ dpg.configure_item("text_controls_group", show=False)
287
+ dpg.configure_item("axis_controls_group", show=False)
288
+ dpg.configure_item("legend_controls_group", show=False)
289
+
290
+ dpg.set_value("selection_text", "")
291
+ dpg.set_value("element_selector_combo", "")
292
+ update_preview(state, dpg)
293
+
294
+
295
+ # EOF
@@ -0,0 +1,93 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: "2026-01-24 (ywatanabe)"
3
+ # File: /home/ywatanabe/proj/scitex-python/src/scitex/canvas/editor/_dearpygui/_state.py
4
+
5
+ """
6
+ Editor state management for DearPyGui editor.
7
+
8
+ Provides EditorState dataclass to hold all editor state.
9
+ """
10
+
11
+ import copy
12
+ from dataclasses import dataclass, field
13
+ from pathlib import Path
14
+ from typing import Any, Dict, List, Optional, Tuple
15
+
16
+
17
+ @dataclass
18
+ class EditorState:
19
+ """Holds all state for the DearPyGui editor."""
20
+
21
+ # Core data
22
+ json_path: Path
23
+ metadata: Dict[str, Any]
24
+ csv_data: Optional[Any] = None
25
+ png_path: Optional[Path] = None
26
+ manual_overrides: Dict[str, Any] = field(default_factory=dict)
27
+
28
+ # Defaults
29
+ scitex_defaults: Dict[str, Any] = field(default_factory=dict)
30
+ metadata_defaults: Dict[str, Any] = field(default_factory=dict)
31
+ current_overrides: Dict[str, Any] = field(default_factory=dict)
32
+
33
+ # Modification tracking
34
+ initial_overrides: Dict[str, Any] = field(default_factory=dict)
35
+ user_modified: bool = False
36
+ texture_id: Optional[int] = None
37
+
38
+ # Selection state
39
+ selected_element: Optional[Dict[str, Any]] = None
40
+ selected_trace_index: Optional[int] = None
41
+
42
+ # Preview bounds
43
+ preview_bounds: Optional[Tuple[int, int, int, int]] = None
44
+ axes_transform: Optional[Tuple] = None
45
+ element_bboxes: Dict[str, Tuple[int, int, int, int]] = field(default_factory=dict)
46
+ element_bboxes_raw: Dict[str, Tuple] = field(default_factory=dict)
47
+
48
+ # Hover state
49
+ hovered_element: Optional[Dict[str, Any]] = None
50
+ last_hover_check: float = 0
51
+ backend_name: str = "dearpygui"
52
+
53
+ # Cached rendering
54
+ cached_base_image: Optional[Any] = None
55
+ cached_base_data: Optional[List[float]] = None
56
+ cache_dirty: bool = True
57
+
58
+ @classmethod
59
+ def create(
60
+ cls,
61
+ json_path: Path,
62
+ metadata: Dict[str, Any],
63
+ csv_data: Optional[Any] = None,
64
+ png_path: Optional[Path] = None,
65
+ manual_overrides: Optional[Dict[str, Any]] = None,
66
+ ) -> "EditorState":
67
+ """Create an EditorState with properly initialized defaults."""
68
+ from .._defaults import extract_defaults_from_metadata, get_scitex_defaults
69
+
70
+ state = cls(
71
+ json_path=Path(json_path),
72
+ metadata=metadata,
73
+ csv_data=csv_data,
74
+ png_path=Path(png_path) if png_path else None,
75
+ manual_overrides=manual_overrides or {},
76
+ )
77
+
78
+ # Get SciTeX defaults and merge with metadata
79
+ state.scitex_defaults = get_scitex_defaults()
80
+ state.metadata_defaults = extract_defaults_from_metadata(metadata)
81
+
82
+ # Start with defaults, then overlay manual overrides
83
+ state.current_overrides = copy.deepcopy(state.scitex_defaults)
84
+ state.current_overrides.update(state.metadata_defaults)
85
+ state.current_overrides.update(state.manual_overrides)
86
+
87
+ # Track modifications
88
+ state.initial_overrides = copy.deepcopy(state.current_overrides)
89
+
90
+ return state
91
+
92
+
93
+ # EOF
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: "2026-01-24 (ywatanabe)"
3
+ # File: /home/ywatanabe/proj/scitex-python/src/scitex/canvas/editor/_dearpygui/_utils.py
4
+
5
+ """
6
+ Utility functions for DearPyGui editor.
7
+
8
+ Provides helper functions like checkerboard pattern creation for transparency preview.
9
+ """
10
+
11
+ from typing import TYPE_CHECKING
12
+
13
+ if TYPE_CHECKING:
14
+ from PIL import Image
15
+
16
+
17
+ def create_checkerboard(width: int, height: int, square_size: int = 10) -> "Image":
18
+ """Create a checkerboard pattern image for transparency preview.
19
+
20
+ Parameters
21
+ ----------
22
+ width : int
23
+ Image width in pixels
24
+ height : int
25
+ Image height in pixels
26
+ square_size : int
27
+ Size of each checkerboard square (default: 10)
28
+
29
+ Returns
30
+ -------
31
+ PIL.Image
32
+ RGBA image with checkerboard pattern (light/dark gray)
33
+ """
34
+ import numpy as np
35
+ from PIL import Image
36
+
37
+ # Create checkerboard pattern
38
+ light_gray = (220, 220, 220, 255)
39
+ dark_gray = (180, 180, 180, 255)
40
+
41
+ # Create array
42
+ img_array = np.zeros((height, width, 4), dtype=np.uint8)
43
+
44
+ for y in range(height):
45
+ for x in range(width):
46
+ # Determine which square we're in
47
+ square_x = x // square_size
48
+ square_y = y // square_size
49
+ if (square_x + square_y) % 2 == 0:
50
+ img_array[y, x] = light_gray
51
+ else:
52
+ img_array[y, x] = dark_gray
53
+
54
+ return Image.fromarray(img_array, "RGBA")
55
+
56
+
57
+ # mm to pt conversion factor
58
+ MM_TO_PT = 2.83465
59
+
60
+
61
+ # EOF
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: "2026-01-24 (ywatanabe)"
3
+ # File: /home/ywatanabe/proj/scitex-python/src/scitex/canvas/editor/flask_editor/_core/__init__.py
4
+
5
+ """Core WebEditor package for Flask-based figure editing.
6
+
7
+ This package provides the WebEditor class and supporting modules for
8
+ browser-based figure editing functionality.
9
+ """
10
+
11
+ from ._bbox_extraction import extract_bboxes_from_metadata
12
+ from ._editor import WebEditor
13
+ from ._export_helpers import compose_panels_to_figure, export_composed_figure
14
+
15
+ # Backward compatibility alias
16
+ _extract_bboxes_from_metadata = extract_bboxes_from_metadata
17
+
18
+ __all__ = [
19
+ "WebEditor",
20
+ "extract_bboxes_from_metadata",
21
+ "export_composed_figure",
22
+ "compose_panels_to_figure",
23
+ "_extract_bboxes_from_metadata",
24
+ ]
25
+
26
+
27
+ # EOF
@@ -0,0 +1,200 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: "2026-01-24 (ywatanabe)"
3
+ # File: /home/ywatanabe/proj/scitex-python/src/scitex/canvas/editor/flask_editor/_core/_bbox_extraction.py
4
+
5
+ """Bounding box extraction from pltz metadata."""
6
+
7
+ from typing import Any, Dict, Optional
8
+
9
+ __all__ = ["extract_bboxes_from_metadata"]
10
+
11
+
12
+ def extract_bboxes_from_metadata(
13
+ metadata: Dict[str, Any],
14
+ display_width: Optional[float] = None,
15
+ display_height: Optional[float] = None,
16
+ ) -> Dict[str, Any]:
17
+ """Extract element bounding boxes from pltz metadata.
18
+
19
+ Builds bboxes from selectable_regions in the metadata for click detection.
20
+ This allows the editor to highlight elements when clicked.
21
+
22
+ Coordinate system (new layered format):
23
+ - selectable_regions bbox_px: Already in final image space (figure_px)
24
+ - Display size: Actual displayed image size (PNG pixels or SVG viewBox)
25
+ - Scale = display_size / figure_px (usually 1:1, but may differ for scaled display)
26
+
27
+ Parameters
28
+ ----------
29
+ metadata : dict
30
+ The pltz JSON metadata containing selectable_regions
31
+ display_width : float, optional
32
+ Actual display image width (from PNG size or SVG viewBox)
33
+ display_height : float, optional
34
+ Actual display image height (from PNG size or SVG viewBox)
35
+
36
+ Returns
37
+ -------
38
+ dict
39
+ Mapping of element IDs to their bounding box coordinates (in display pixels)
40
+ """
41
+ bboxes = {}
42
+ selectable = metadata.get("selectable_regions", {})
43
+
44
+ # Figure dimensions from new layered format (bbox_px are in this space)
45
+ figure_px = metadata.get("figure_px", [])
46
+ if isinstance(figure_px, list) and len(figure_px) >= 2:
47
+ fig_width = figure_px[0]
48
+ fig_height = figure_px[1]
49
+ else:
50
+ # Fallback for old format: try hit_regions.path_data.figure
51
+ hit_regions = metadata.get("hit_regions", {})
52
+ path_data = hit_regions.get("path_data", {})
53
+ orig_fig = path_data.get("figure", {})
54
+ fig_width = orig_fig.get("width_px", 944)
55
+ fig_height = orig_fig.get("height_px", 803)
56
+
57
+ # Use actual display dimensions if provided, else use figure_px
58
+ if display_width is None:
59
+ display_width = fig_width
60
+ if display_height is None:
61
+ display_height = fig_height
62
+
63
+ # Scale factor: display / figure_px
64
+ scale_x = display_width / fig_width if fig_width > 0 else 1
65
+ scale_y = display_height / fig_height if fig_height > 0 else 1
66
+
67
+ def to_display_bbox(bbox, is_list=True):
68
+ """Convert bbox to display pixels."""
69
+ if is_list:
70
+ x0, y0, x1, y1 = bbox[0], bbox[1], bbox[2], bbox[3]
71
+ else:
72
+ x0 = bbox.get("x0", 0)
73
+ y0 = bbox.get("y0", 0)
74
+ x1 = bbox.get("x1", bbox.get("x0", 0) + bbox.get("width", 0))
75
+ y1 = bbox.get("y1", bbox.get("y0", 0) + bbox.get("height", 0))
76
+
77
+ disp_x0 = x0 * scale_x
78
+ disp_x1 = x1 * scale_x
79
+ disp_y0 = y0 * scale_y
80
+ disp_y1 = y1 * scale_y
81
+
82
+ return {
83
+ "x0": disp_x0,
84
+ "y0": disp_y0,
85
+ "x1": disp_x1,
86
+ "y1": disp_y1,
87
+ "x": disp_x0,
88
+ "y": disp_y0,
89
+ "width": disp_x1 - disp_x0,
90
+ "height": disp_y1 - disp_y0,
91
+ }
92
+
93
+ # Extract from selectable_regions.axes
94
+ axes_regions = selectable.get("axes", [])
95
+ for ax_idx, ax in enumerate(axes_regions):
96
+ ax_key = f"ax_{ax_idx:02d}"
97
+
98
+ # Title
99
+ title = ax.get("title", {})
100
+ if title and "bbox_px" in title:
101
+ bbox_disp = to_display_bbox(title["bbox_px"])
102
+ bboxes[f"{ax_key}_title"] = {
103
+ **bbox_disp,
104
+ "type": "title",
105
+ "text": title.get("text", ""),
106
+ }
107
+
108
+ # X label
109
+ xlabel = ax.get("xlabel", {})
110
+ if xlabel and "bbox_px" in xlabel:
111
+ bbox_disp = to_display_bbox(xlabel["bbox_px"])
112
+ bboxes[f"{ax_key}_xlabel"] = {
113
+ **bbox_disp,
114
+ "type": "xlabel",
115
+ "text": xlabel.get("text", ""),
116
+ }
117
+
118
+ # Y label
119
+ ylabel = ax.get("ylabel", {})
120
+ if ylabel and "bbox_px" in ylabel:
121
+ bbox_disp = to_display_bbox(ylabel["bbox_px"])
122
+ bboxes[f"{ax_key}_ylabel"] = {
123
+ **bbox_disp,
124
+ "type": "ylabel",
125
+ "text": ylabel.get("text", ""),
126
+ }
127
+
128
+ # Legend
129
+ legend = ax.get("legend", {})
130
+ if legend and "bbox_px" in legend:
131
+ bbox_disp = to_display_bbox(legend["bbox_px"])
132
+ bboxes[f"{ax_key}_legend"] = {
133
+ **bbox_disp,
134
+ "type": "legend",
135
+ }
136
+
137
+ # X-axis spine
138
+ xaxis = ax.get("xaxis", {})
139
+ if xaxis:
140
+ spine = xaxis.get("spine", {})
141
+ if spine and "bbox_px" in spine:
142
+ bbox_disp = to_display_bbox(spine["bbox_px"])
143
+ bboxes[f"{ax_key}_xaxis_spine"] = {
144
+ **bbox_disp,
145
+ "type": "xaxis",
146
+ }
147
+
148
+ # Y-axis spine
149
+ yaxis = ax.get("yaxis", {})
150
+ if yaxis:
151
+ spine = yaxis.get("spine", {})
152
+ if spine and "bbox_px" in spine:
153
+ bbox_disp = to_display_bbox(spine["bbox_px"])
154
+ bboxes[f"{ax_key}_yaxis_spine"] = {
155
+ **bbox_disp,
156
+ "type": "yaxis",
157
+ }
158
+
159
+ # Extract traces from artists
160
+ artists = metadata.get("artists", [])
161
+ if not artists:
162
+ hit_regions = metadata.get("hit_regions", {})
163
+ path_data = hit_regions.get("path_data", {})
164
+ artists = path_data.get("artists", [])
165
+
166
+ for artist in artists:
167
+ artist_id = artist.get("id", 0)
168
+ artist_type = artist.get("type", "line")
169
+ bbox_px = artist.get("bbox_px", {})
170
+ if bbox_px:
171
+ bbox_disp = to_display_bbox(bbox_px, is_list=False)
172
+ trace_entry = {
173
+ **bbox_disp,
174
+ "type": artist_type,
175
+ "label": artist.get("label", f"Trace {artist_id}"),
176
+ "element_type": artist_type,
177
+ }
178
+
179
+ path_px = artist.get("path_px", [])
180
+ if path_px:
181
+ scaled_points = [
182
+ [pt[0] * scale_x, pt[1] * scale_y] for pt in path_px if len(pt) >= 2
183
+ ]
184
+ trace_entry["points"] = scaled_points
185
+
186
+ bboxes[f"trace_{artist_id}"] = trace_entry
187
+
188
+ bboxes["_meta"] = {
189
+ "display_width": display_width,
190
+ "display_height": display_height,
191
+ "figure_px_width": fig_width,
192
+ "figure_px_height": fig_height,
193
+ "scale_x": scale_x,
194
+ "scale_y": scale_y,
195
+ }
196
+
197
+ return bboxes
198
+
199
+
200
+ # EOF