scitex 2.7.3__py3-none-any.whl → 2.8.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 (160) hide show
  1. scitex/__version__.py +1 -1
  2. scitex/dev/plt/__init__.py +0 -0
  3. scitex/dev/plt/plot_mpl_axhline.py +0 -0
  4. scitex/dev/plt/plot_mpl_axhspan.py +0 -0
  5. scitex/dev/plt/plot_mpl_axvline.py +0 -0
  6. scitex/dev/plt/plot_mpl_axvspan.py +0 -0
  7. scitex/dev/plt/plot_mpl_bar.py +0 -0
  8. scitex/dev/plt/plot_mpl_barh.py +0 -0
  9. scitex/dev/plt/plot_mpl_boxplot.py +0 -0
  10. scitex/dev/plt/plot_mpl_contour.py +0 -0
  11. scitex/dev/plt/plot_mpl_contourf.py +0 -0
  12. scitex/dev/plt/plot_mpl_errorbar.py +0 -0
  13. scitex/dev/plt/plot_mpl_eventplot.py +0 -0
  14. scitex/dev/plt/plot_mpl_fill.py +0 -0
  15. scitex/dev/plt/plot_mpl_fill_between.py +0 -0
  16. scitex/dev/plt/plot_mpl_hexbin.py +0 -0
  17. scitex/dev/plt/plot_mpl_hist.py +0 -0
  18. scitex/dev/plt/plot_mpl_hist2d.py +0 -0
  19. scitex/dev/plt/plot_mpl_imshow.py +0 -0
  20. scitex/dev/plt/plot_mpl_pcolormesh.py +0 -0
  21. scitex/dev/plt/plot_mpl_pie.py +0 -0
  22. scitex/dev/plt/plot_mpl_plot.py +0 -0
  23. scitex/dev/plt/plot_mpl_quiver.py +0 -0
  24. scitex/dev/plt/plot_mpl_scatter.py +0 -0
  25. scitex/dev/plt/plot_mpl_stackplot.py +0 -0
  26. scitex/dev/plt/plot_mpl_stem.py +0 -0
  27. scitex/dev/plt/plot_mpl_step.py +0 -0
  28. scitex/dev/plt/plot_mpl_violinplot.py +0 -0
  29. scitex/dev/plt/plot_sns_barplot.py +0 -0
  30. scitex/dev/plt/plot_sns_boxplot.py +0 -0
  31. scitex/dev/plt/plot_sns_heatmap.py +0 -0
  32. scitex/dev/plt/plot_sns_histplot.py +0 -0
  33. scitex/dev/plt/plot_sns_kdeplot.py +0 -0
  34. scitex/dev/plt/plot_sns_lineplot.py +0 -0
  35. scitex/dev/plt/plot_sns_scatterplot.py +0 -0
  36. scitex/dev/plt/plot_sns_stripplot.py +0 -0
  37. scitex/dev/plt/plot_sns_swarmplot.py +0 -0
  38. scitex/dev/plt/plot_sns_violinplot.py +0 -0
  39. scitex/dev/plt/plot_stx_bar.py +0 -0
  40. scitex/dev/plt/plot_stx_barh.py +0 -0
  41. scitex/dev/plt/plot_stx_box.py +0 -0
  42. scitex/dev/plt/plot_stx_boxplot.py +0 -0
  43. scitex/dev/plt/plot_stx_conf_mat.py +0 -0
  44. scitex/dev/plt/plot_stx_contour.py +0 -0
  45. scitex/dev/plt/plot_stx_ecdf.py +0 -0
  46. scitex/dev/plt/plot_stx_errorbar.py +0 -0
  47. scitex/dev/plt/plot_stx_fill_between.py +0 -0
  48. scitex/dev/plt/plot_stx_fillv.py +0 -0
  49. scitex/dev/plt/plot_stx_heatmap.py +0 -0
  50. scitex/dev/plt/plot_stx_image.py +0 -0
  51. scitex/dev/plt/plot_stx_imshow.py +0 -0
  52. scitex/dev/plt/plot_stx_joyplot.py +0 -0
  53. scitex/dev/plt/plot_stx_kde.py +0 -0
  54. scitex/dev/plt/plot_stx_line.py +0 -0
  55. scitex/dev/plt/plot_stx_mean_ci.py +0 -0
  56. scitex/dev/plt/plot_stx_mean_std.py +0 -0
  57. scitex/dev/plt/plot_stx_median_iqr.py +0 -0
  58. scitex/dev/plt/plot_stx_raster.py +0 -0
  59. scitex/dev/plt/plot_stx_rectangle.py +0 -0
  60. scitex/dev/plt/plot_stx_scatter.py +0 -0
  61. scitex/dev/plt/plot_stx_shaded_line.py +0 -0
  62. scitex/dev/plt/plot_stx_violin.py +0 -0
  63. scitex/dev/plt/plot_stx_violinplot.py +0 -0
  64. scitex/diagram/README.md +197 -0
  65. scitex/diagram/__init__.py +48 -0
  66. scitex/diagram/_compile.py +312 -0
  67. scitex/diagram/_diagram.py +355 -0
  68. scitex/diagram/_presets.py +173 -0
  69. scitex/diagram/_schema.py +182 -0
  70. scitex/diagram/_split.py +278 -0
  71. scitex/fig/editor/__init__.py +5 -2
  72. scitex/fig/editor/_dearpygui_editor.py +1 -1
  73. scitex/fig/editor/_mpl_editor.py +1 -1
  74. scitex/fig/editor/_qt_editor.py +1 -1
  75. scitex/fig/editor/_tkinter_editor.py +1 -1
  76. scitex/fig/editor/edit/__init__.py +50 -0
  77. scitex/fig/editor/edit/backend_detector.py +109 -0
  78. scitex/fig/editor/edit/bundle_resolver.py +240 -0
  79. scitex/fig/editor/edit/editor_launcher.py +239 -0
  80. scitex/fig/editor/edit/manual_handler.py +53 -0
  81. scitex/fig/editor/edit/panel_loader.py +232 -0
  82. scitex/fig/editor/edit/path_resolver.py +67 -0
  83. scitex/fig/editor/flask_editor/_bbox.py +23 -0
  84. scitex/fig/editor/flask_editor/_core.py +908 -103
  85. scitex/fig/editor/flask_editor/_renderer.py +74 -0
  86. scitex/fig/editor/flask_editor/static/css/base/reset.css +41 -0
  87. scitex/fig/editor/flask_editor/static/css/base/typography.css +16 -0
  88. scitex/fig/editor/flask_editor/static/css/base/variables.css +85 -0
  89. scitex/fig/editor/flask_editor/static/css/components/buttons.css +217 -0
  90. scitex/fig/editor/flask_editor/static/css/components/context-menu.css +93 -0
  91. scitex/fig/editor/flask_editor/static/css/components/dropdown.css +57 -0
  92. scitex/fig/editor/flask_editor/static/css/components/forms.css +112 -0
  93. scitex/fig/editor/flask_editor/static/css/components/modal.css +59 -0
  94. scitex/fig/editor/flask_editor/static/css/components/sections.css +212 -0
  95. scitex/fig/editor/flask_editor/static/css/features/canvas.css +176 -0
  96. scitex/fig/editor/flask_editor/static/css/features/element-inspector.css +190 -0
  97. scitex/fig/editor/flask_editor/static/css/features/loading.css +59 -0
  98. scitex/fig/editor/flask_editor/static/css/features/overlay.css +45 -0
  99. scitex/fig/editor/flask_editor/static/css/features/panel-grid.css +95 -0
  100. scitex/fig/editor/flask_editor/static/css/features/selection.css +101 -0
  101. scitex/fig/editor/flask_editor/static/css/features/statistics.css +138 -0
  102. scitex/fig/editor/flask_editor/static/css/index.css +31 -0
  103. scitex/fig/editor/flask_editor/static/css/layout/container.css +7 -0
  104. scitex/fig/editor/flask_editor/static/css/layout/controls.css +56 -0
  105. scitex/fig/editor/flask_editor/static/css/layout/preview.css +78 -0
  106. scitex/fig/editor/flask_editor/static/js/alignment/axis.js +314 -0
  107. scitex/fig/editor/flask_editor/static/js/alignment/basic.js +107 -0
  108. scitex/fig/editor/flask_editor/static/js/alignment/distribute.js +54 -0
  109. scitex/fig/editor/flask_editor/static/js/canvas/canvas.js +172 -0
  110. scitex/fig/editor/flask_editor/static/js/canvas/dragging.js +258 -0
  111. scitex/fig/editor/flask_editor/static/js/canvas/resize.js +48 -0
  112. scitex/fig/editor/flask_editor/static/js/canvas/selection.js +71 -0
  113. scitex/fig/editor/flask_editor/static/js/core/api.js +288 -0
  114. scitex/fig/editor/flask_editor/static/js/core/state.js +143 -0
  115. scitex/fig/editor/flask_editor/static/js/core/utils.js +245 -0
  116. scitex/fig/editor/flask_editor/static/js/dev/element-inspector.js +992 -0
  117. scitex/fig/editor/flask_editor/static/js/editor/bbox.js +339 -0
  118. scitex/fig/editor/flask_editor/static/js/editor/element-drag.js +286 -0
  119. scitex/fig/editor/flask_editor/static/js/editor/overlay.js +371 -0
  120. scitex/fig/editor/flask_editor/static/js/editor/preview.js +293 -0
  121. scitex/fig/editor/flask_editor/static/js/main.js +426 -0
  122. scitex/fig/editor/flask_editor/static/js/shortcuts/context-menu.js +152 -0
  123. scitex/fig/editor/flask_editor/static/js/shortcuts/keyboard.js +265 -0
  124. scitex/fig/editor/flask_editor/static/js/ui/controls.js +184 -0
  125. scitex/fig/editor/flask_editor/static/js/ui/download.js +57 -0
  126. scitex/fig/editor/flask_editor/static/js/ui/help.js +100 -0
  127. scitex/fig/editor/flask_editor/static/js/ui/theme.js +34 -0
  128. scitex/fig/editor/flask_editor/templates/__init__.py +95 -5
  129. scitex/fig/editor/flask_editor/templates/_html.py +27 -9
  130. scitex/fig/editor/flask_editor/templates/_scripts.py +1928 -131
  131. scitex/fig/editor/flask_editor/templates/_styles.py +363 -51
  132. scitex/fig/io/_bundle.py +97 -12
  133. scitex/io/__init__.py +12 -0
  134. scitex/io/_bundle.py +69 -10
  135. scitex/io/_zip_bundle.py +439 -0
  136. scitex/plt/_subplots/_AxisWrapperMixins/_AdjustmentMixin/__init__.py +0 -0
  137. scitex/plt/_subplots/_AxisWrapperMixins/_AdjustmentMixin/_labels.py +0 -0
  138. scitex/plt/_subplots/_AxisWrapperMixins/_AdjustmentMixin/_metadata.py +0 -0
  139. scitex/plt/_subplots/_AxisWrapperMixins/_AdjustmentMixin/_visual.py +0 -0
  140. scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin/__init__.py +0 -0
  141. scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin/_base.py +0 -0
  142. scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin/_scientific.py +0 -0
  143. scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin/_statistical.py +0 -0
  144. scitex/plt/_subplots/_AxisWrapperMixins/_MatplotlibPlotMixin/_stx_aliases.py +0 -0
  145. scitex/plt/_subplots/_AxisWrapperMixins/_RawMatplotlibMixin.py +0 -0
  146. scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin/__init__.py +0 -0
  147. scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin/_base.py +0 -0
  148. scitex/plt/_subplots/_AxisWrapperMixins/_SeabornMixin/_wrappers.py +0 -0
  149. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_bar.py +0 -0
  150. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_barh.py +0 -0
  151. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_errorbar.py +0 -0
  152. scitex/plt/_subplots/_export_as_csv_formatters/_format_stx_scatter.py +0 -0
  153. scitex/plt/io/_layered_bundle.py +0 -0
  154. scitex/schema/_plot.py +0 -0
  155. {scitex-2.7.3.dist-info → scitex-2.8.1.dist-info}/METADATA +1 -1
  156. {scitex-2.7.3.dist-info → scitex-2.8.1.dist-info}/RECORD +78 -22
  157. scitex/fig/editor/_edit.py +0 -751
  158. {scitex-2.7.3.dist-info → scitex-2.8.1.dist-info}/WHEEL +0 -0
  159. {scitex-2.7.3.dist-info → scitex-2.8.1.dist-info}/entry_points.txt +0 -0
  160. {scitex-2.7.3.dist-info → scitex-2.8.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,232 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # Timestamp: "2025-12-14 (ywatanabe)"
4
+ # File: /home/ywatanabe/proj/scitex-code/src/scitex/fig/editor/edit/panel_loader.py
5
+
6
+ """Panel data loading for figure editor."""
7
+
8
+ import io
9
+ import json as json_module
10
+ from pathlib import Path
11
+ from typing import Optional, Union, Dict, Any
12
+
13
+ __all__ = ["load_panel_data"]
14
+
15
+
16
+ def load_panel_data(panel_path: Union[Path, str], is_zip: bool = None) -> Optional[Dict[str, Any]]:
17
+ """
18
+ Load panel data from either a .pltz.d directory or a .pltz zip file.
19
+
20
+ Handles both formats transparently using in-memory reading for zips.
21
+
22
+ Parameters
23
+ ----------
24
+ panel_path : Path or str
25
+ Path to .pltz.d directory or .pltz zip file
26
+ is_zip : bool, optional
27
+ If True, treat as zip file. If False, treat as directory.
28
+ If None, auto-detect based on path suffix and existence.
29
+
30
+ Returns
31
+ -------
32
+ dict or None
33
+ Dictionary with keys: metadata, csv_data, png_bytes, hitmap_bytes, img_size
34
+ For directories, also includes: json_path, png_path, hitmap_path
35
+ Returns None if panel cannot be loaded
36
+ """
37
+ from PIL import Image
38
+
39
+ panel_path = Path(panel_path)
40
+
41
+ # Auto-detect if not specified
42
+ if is_zip is None:
43
+ spath = str(panel_path)
44
+ if spath.endswith('.pltz') and not spath.endswith('.pltz.d'):
45
+ is_zip = panel_path.is_file()
46
+ else:
47
+ is_zip = False
48
+
49
+ if is_zip:
50
+ return _load_from_zip(panel_path)
51
+ else:
52
+ return _load_from_directory(panel_path)
53
+
54
+
55
+ def _load_from_zip(panel_path: Path) -> Optional[Dict[str, Any]]:
56
+ """Load panel data from a .pltz zip file."""
57
+ from PIL import Image
58
+ from scitex.io._zip_bundle import ZipBundle
59
+
60
+ if not panel_path.exists():
61
+ return None
62
+
63
+ try:
64
+ with ZipBundle(panel_path, mode="r") as zb:
65
+ # Load spec.json for metadata
66
+ try:
67
+ metadata = zb.read_json("spec.json")
68
+ except FileNotFoundError:
69
+ metadata = {}
70
+
71
+ # Load style.json if exists
72
+ try:
73
+ style = zb.read_json("style.json")
74
+ metadata["style"] = style
75
+ except FileNotFoundError:
76
+ pass
77
+
78
+ # Find and read PNG
79
+ png_bytes = None
80
+ for name in zb.namelist():
81
+ if name.endswith('.png') and '_hitmap' not in name and '_overview' not in name:
82
+ if 'exports/' in name:
83
+ png_bytes = zb.read_bytes(name)
84
+ break
85
+
86
+ # If no PNG in exports/, try root level
87
+ if not png_bytes:
88
+ for name in zb.namelist():
89
+ if name.endswith('.png') and '_hitmap' not in name and '_overview' not in name:
90
+ png_bytes = zb.read_bytes(name)
91
+ break
92
+
93
+ # Get image size
94
+ img_size = None
95
+ if png_bytes:
96
+ img = Image.open(io.BytesIO(png_bytes))
97
+ img_size = {"width": img.size[0], "height": img.size[1]}
98
+ img.close()
99
+
100
+ # Find and read hitmap
101
+ hitmap_bytes = None
102
+ for name in zb.namelist():
103
+ if '_hitmap.png' in name:
104
+ hitmap_bytes = zb.read_bytes(name)
105
+ break
106
+
107
+ # Load geometry_px.json if available
108
+ geometry_data = None
109
+ try:
110
+ geometry_data = zb.read_json("cache/geometry_px.json")
111
+ except FileNotFoundError:
112
+ pass
113
+
114
+ return {
115
+ "metadata": metadata,
116
+ "png_bytes": png_bytes,
117
+ "hitmap_bytes": hitmap_bytes,
118
+ "img_size": img_size,
119
+ "geometry_data": geometry_data,
120
+ "is_zip": True,
121
+ }
122
+ except Exception as e:
123
+ print(f"Error loading panel zip {panel_path}: {e}")
124
+ return None
125
+
126
+
127
+ def _load_from_directory(panel_path: Path) -> Optional[Dict[str, Any]]:
128
+ """Load panel data from a .pltz.d directory."""
129
+ import scitex as stx
130
+
131
+ panel_dir = panel_path
132
+ if not panel_dir.exists():
133
+ return None
134
+
135
+ # Check for layered vs legacy format
136
+ spec_path = panel_dir / "spec.json"
137
+ if spec_path.exists():
138
+ return _load_layered_directory(panel_dir)
139
+ else:
140
+ return _load_legacy_directory(panel_dir)
141
+
142
+
143
+ def _load_layered_directory(panel_dir: Path) -> Dict[str, Any]:
144
+ """Load panel data from layered format directory."""
145
+ import scitex as stx
146
+ from scitex.plt.io import load_layered_pltz_bundle
147
+
148
+ bundle_data = load_layered_pltz_bundle(panel_dir)
149
+ metadata = bundle_data.get("merged", {})
150
+
151
+ # Find CSV
152
+ csv_data = None
153
+ for f in panel_dir.glob("*.csv"):
154
+ csv_data = stx.io.load(f)
155
+ break
156
+
157
+ # Find exports - prefer PNG over SVG (PIL can't open SVG)
158
+ png_path = None
159
+ svg_path = None
160
+ hitmap_path = None
161
+ exports_dir = panel_dir / "exports"
162
+ if exports_dir.exists():
163
+ for f in exports_dir.iterdir():
164
+ name = f.name
165
+ if name.endswith('_hitmap.png'):
166
+ hitmap_path = f
167
+ elif name.endswith('.png') and '_hitmap' not in name and '_overview' not in name:
168
+ png_path = f
169
+ elif name.endswith('.svg') and '_hitmap' not in name and svg_path is None:
170
+ svg_path = f
171
+
172
+ if png_path is None:
173
+ png_path = svg_path
174
+
175
+ # Load geometry_px.json if available
176
+ geometry_data = None
177
+ geometry_path = panel_dir / "cache" / "geometry_px.json"
178
+ if geometry_path.exists():
179
+ with open(geometry_path) as f:
180
+ geometry_data = json_module.load(f)
181
+
182
+ return {
183
+ "json_path": panel_dir / "spec.json",
184
+ "metadata": metadata,
185
+ "csv_data": csv_data,
186
+ "png_path": png_path,
187
+ "hitmap_path": hitmap_path,
188
+ "geometry_data": geometry_data,
189
+ "is_zip": False,
190
+ }
191
+
192
+
193
+ def _load_legacy_directory(panel_dir: Path) -> Optional[Dict[str, Any]]:
194
+ """Load panel data from legacy format directory."""
195
+ import scitex as stx
196
+
197
+ json_path = None
198
+ csv_data = None
199
+ png_path = None
200
+ hitmap_path = None
201
+
202
+ for f in panel_dir.iterdir():
203
+ name = f.name
204
+ if name.endswith('.json') and not name.endswith('.manual.json'):
205
+ json_path = f
206
+ elif name.endswith('.csv'):
207
+ csv_data = stx.io.load(f)
208
+ elif name.endswith('_hitmap.png'):
209
+ hitmap_path = f
210
+ elif name.endswith('.svg') and '_hitmap' not in name:
211
+ png_path = f
212
+ elif name.endswith('.png') and '_hitmap' not in name and '_overview' not in name:
213
+ if png_path is None:
214
+ png_path = f
215
+
216
+ if json_path is None:
217
+ return None
218
+
219
+ with open(json_path, 'r') as f:
220
+ metadata = json_module.load(f)
221
+
222
+ return {
223
+ "json_path": json_path,
224
+ "metadata": metadata,
225
+ "csv_data": csv_data,
226
+ "png_path": png_path,
227
+ "hitmap_path": hitmap_path,
228
+ "is_zip": False,
229
+ }
230
+
231
+
232
+ # EOF
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ # Timestamp: "2025-12-14 (ywatanabe)"
4
+ # File: /home/ywatanabe/proj/scitex-code/src/scitex/fig/editor/edit/path_resolver.py
5
+
6
+ """Basic path resolution for figure files."""
7
+
8
+ from pathlib import Path
9
+ from typing import Optional, Tuple
10
+
11
+ __all__ = ["resolve_figure_paths"]
12
+
13
+
14
+ def resolve_figure_paths(path: Path) -> Tuple[Path, Optional[Path], Optional[Path]]:
15
+ """
16
+ Resolve JSON, CSV, and PNG paths from any input file path.
17
+
18
+ Handles two patterns:
19
+ 1. Flat (sibling): path/to/figure.{json,csv,png}
20
+ 2. Organized (subdirs): path/to/{json,csv,png}/figure.{ext}
21
+
22
+ Parameters
23
+ ----------
24
+ path : Path
25
+ Input path (can be JSON, CSV, or PNG)
26
+
27
+ Returns
28
+ -------
29
+ tuple
30
+ (json_path, csv_path, png_path) - csv_path/png_path may be None if not found
31
+ """
32
+ path = Path(path)
33
+ stem = path.stem
34
+ parent = path.parent
35
+
36
+ # Check if this is organized pattern (parent is json/, csv/, png/)
37
+ if parent.name in ("json", "csv", "png"):
38
+ base_dir = parent.parent
39
+ json_path = base_dir / "json" / f"{stem}.json"
40
+ csv_path = base_dir / "csv" / f"{stem}.csv"
41
+ png_path = base_dir / "png" / f"{stem}.png"
42
+ else:
43
+ # Flat pattern - sibling files
44
+ json_path = parent / f"{stem}.json"
45
+ csv_path = parent / f"{stem}.csv"
46
+ png_path = parent / f"{stem}.png"
47
+
48
+ # If input was .manual.json, get base json
49
+ if stem.endswith(".manual"):
50
+ base_stem = stem[:-7] # Remove '.manual'
51
+ if parent.name == "json":
52
+ json_path = parent / f"{base_stem}.json"
53
+ csv_path = parent.parent / "csv" / f"{base_stem}.csv"
54
+ png_path = parent.parent / "png" / f"{base_stem}.png"
55
+ else:
56
+ json_path = parent / f"{base_stem}.json"
57
+ csv_path = parent / f"{base_stem}.csv"
58
+ png_path = parent / f"{base_stem}.png"
59
+
60
+ return (
61
+ json_path,
62
+ csv_path if csv_path.exists() else None,
63
+ png_path if png_path.exists() else None,
64
+ )
65
+
66
+
67
+ # EOF
@@ -290,6 +290,29 @@ def extract_bboxes_multi(
290
290
  legend = ax.get_legend()
291
291
  if legend:
292
292
  get_element_bbox(legend, "legend", ax_id, ax)
293
+ # Add element_type for drag detection
294
+ if f"{ax_id}_legend" in bboxes:
295
+ bboxes[f"{ax_id}_legend"]["element_type"] = "legend"
296
+ bboxes[f"{ax_id}_legend"]["draggable"] = True
297
+
298
+ # Get panel letter (text annotations like A, B, C)
299
+ import re
300
+ panel_letter_pattern = re.compile(r'^[A-Z]\.?$|^\([A-Za-z]\)$')
301
+ for idx, text_artist in enumerate(ax.texts):
302
+ text_content = text_artist.get_text().strip()
303
+ if text_content and panel_letter_pattern.match(text_content):
304
+ name = f"panel_letter_{text_content.replace('.', '').replace('(', '').replace(')', '')}"
305
+ get_element_bbox(text_artist, name, ax_id, ax)
306
+ full_name = f"{ax_id}_{name}"
307
+ if full_name in bboxes:
308
+ bboxes[full_name]["element_type"] = "panel_letter"
309
+ bboxes[full_name]["draggable"] = True
310
+ bboxes[full_name]["text"] = text_content
311
+ # Get position in axes coordinates (0-1)
312
+ pos = text_artist.get_position()
313
+ transform = text_artist.get_transform()
314
+ if transform == ax.transAxes:
315
+ bboxes[full_name]["axes_position"] = {"x": pos[0], "y": pos[1]}
293
316
 
294
317
  # Get trace (line) bboxes
295
318
  _extract_trace_bboxes_for_axis(