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,339 @@
1
+ /**
2
+ * Bounding Box Detection
3
+ * Element hit detection and proximity calculations
4
+ */
5
+
6
+ // ============================================================================
7
+ // Hit Detection - Main Entry Point
8
+ // ============================================================================
9
+ function findElementAt(x, y) {
10
+ // Multi-panel aware hit detection with specificity hierarchy:
11
+ // 1. Data elements with legacy points - proximity detection (correct saved-image coords)
12
+ // 2. Small elements (labels, ticks, legends, bars, fills)
13
+ // 3. Panel bboxes - lowest priority (fallback)
14
+
15
+ const PROXIMITY_THRESHOLD = 15;
16
+ const SCATTER_THRESHOLD = 20; // Larger threshold for scatter points
17
+
18
+ // First: Check for data elements using legacy points (in saved-image coordinates)
19
+ let closestDataElement = null;
20
+ let minDistance = Infinity;
21
+
22
+ for (const [name, bbox] of Object.entries(elementBboxes)) {
23
+ if (name === '_meta') continue; // Skip metadata entry
24
+
25
+ // Prioritize legacy points array (already in correct saved-image coordinates)
26
+ if (bbox.points && bbox.points.length > 0) {
27
+ // Check if cursor is within general bbox area first
28
+ if (x >= bbox.x0 - SCATTER_THRESHOLD && x <= bbox.x1 + SCATTER_THRESHOLD &&
29
+ y >= bbox.y0 - SCATTER_THRESHOLD && y <= bbox.y1 + SCATTER_THRESHOLD) {
30
+
31
+ const elementType = bbox.element_type || 'line';
32
+ let dist;
33
+
34
+ if (elementType === 'scatter') {
35
+ // For scatter, find distance to nearest point
36
+ dist = distanceToNearestPoint(x, y, bbox.points);
37
+ } else {
38
+ // For lines, find distance to line segments
39
+ dist = distanceToLine(x, y, bbox.points);
40
+ }
41
+
42
+ if (dist < minDistance) {
43
+ minDistance = dist;
44
+ closestDataElement = name;
45
+ }
46
+ }
47
+ }
48
+ }
49
+
50
+ // Use appropriate threshold based on element type
51
+ if (closestDataElement) {
52
+ const bbox = elementBboxes[closestDataElement];
53
+ const threshold = (bbox.element_type === 'scatter') ? SCATTER_THRESHOLD : PROXIMITY_THRESHOLD;
54
+ if (minDistance <= threshold) {
55
+ return closestDataElement;
56
+ }
57
+ }
58
+
59
+ // Second: Collect all bbox matches, excluding panels and data elements with points
60
+ const elementMatches = [];
61
+ const panelMatches = [];
62
+
63
+ for (const [name, bbox] of Object.entries(elementBboxes)) {
64
+ if (x >= bbox.x0 && x <= bbox.x1 && y >= bbox.y0 && y <= bbox.y1) {
65
+ const area = (bbox.x1 - bbox.x0) * (bbox.y1 - bbox.y0);
66
+ const isPanel = bbox.is_panel || name.endsWith('_panel');
67
+ const hasPoints = bbox.points && bbox.points.length > 0;
68
+
69
+ if (hasPoints) {
70
+ // Already handled above with proximity
71
+ continue;
72
+ } else if (isPanel) {
73
+ panelMatches.push({name, area, bbox});
74
+ } else {
75
+ elementMatches.push({name, area, bbox});
76
+ }
77
+ }
78
+ }
79
+
80
+ // Return smallest non-panel element if any
81
+ if (elementMatches.length > 0) {
82
+ elementMatches.sort((a, b) => a.area - b.area);
83
+ return elementMatches[0].name;
84
+ }
85
+
86
+ // Fallback to panel selection (useful for multi-panel figures)
87
+ if (panelMatches.length > 0) {
88
+ panelMatches.sort((a, b) => a.area - b.area);
89
+ return panelMatches[0].name;
90
+ }
91
+
92
+ return null;
93
+ }
94
+
95
+ // ============================================================================
96
+ // Find All Overlapping Elements (for cycle selection)
97
+ // ============================================================================
98
+ function findAllElementsAt(x, y) {
99
+ // Find all elements at cursor position (for cycle selection)
100
+ // Returns array sorted by specificity (most specific first)
101
+ const PROXIMITY_THRESHOLD = 15;
102
+ const SCATTER_THRESHOLD = 20;
103
+
104
+ const results = [];
105
+
106
+ for (const [name, bbox] of Object.entries(elementBboxes)) {
107
+ let match = false;
108
+ let distance = Infinity;
109
+ let priority = 0; // Lower = more specific
110
+
111
+ const hasPoints = bbox.points && bbox.points.length > 0;
112
+ const elementType = bbox.element_type || '';
113
+ const isPanel = bbox.is_panel || name.endsWith('_panel');
114
+
115
+ // Check data elements with points (lines, scatter)
116
+ if (hasPoints) {
117
+ if (x >= bbox.x0 - SCATTER_THRESHOLD && x <= bbox.x1 + SCATTER_THRESHOLD &&
118
+ y >= bbox.y0 - SCATTER_THRESHOLD && y <= bbox.y1 + SCATTER_THRESHOLD) {
119
+
120
+ if (elementType === 'scatter') {
121
+ distance = distanceToNearestPoint(x, y, bbox.points);
122
+ if (distance <= SCATTER_THRESHOLD) {
123
+ match = true;
124
+ priority = 1; // Scatter points = high priority
125
+ }
126
+ } else {
127
+ distance = distanceToLine(x, y, bbox.points);
128
+ if (distance <= PROXIMITY_THRESHOLD) {
129
+ match = true;
130
+ priority = 2; // Lines = high priority
131
+ }
132
+ }
133
+ }
134
+ }
135
+
136
+ // Check bbox containment
137
+ if (x >= bbox.x0 && x <= bbox.x1 && y >= bbox.y0 && y <= bbox.y1) {
138
+ const area = (bbox.x1 - bbox.x0) * (bbox.y1 - bbox.y0);
139
+
140
+ if (!match) {
141
+ match = true;
142
+ distance = 0;
143
+ }
144
+
145
+ if (isPanel) {
146
+ priority = 100; // Panels = lowest priority
147
+ } else if (!hasPoints) {
148
+ // Small elements like labels, ticks - use area for priority
149
+ priority = 10 + Math.min(area / 10000, 50);
150
+ }
151
+ }
152
+
153
+ if (match) {
154
+ results.push({ name, distance, priority, bbox });
155
+ }
156
+ }
157
+
158
+ // Sort by priority (lower first), then by distance
159
+ results.sort((a, b) => {
160
+ if (a.priority !== b.priority) return a.priority - b.priority;
161
+ return a.distance - b.distance;
162
+ });
163
+
164
+ return results.map(r => r.name);
165
+ }
166
+
167
+ // ============================================================================
168
+ // Panel Element Detection (for multi-panel canvas)
169
+ // ============================================================================
170
+ function findElementInPanelAt(x, y, bboxes) {
171
+ const PROXIMITY_THRESHOLD = 15;
172
+ const SCATTER_THRESHOLD = 20;
173
+
174
+ let closestDataElement = null;
175
+ let minDistance = Infinity;
176
+
177
+ // Check data elements with points
178
+ for (const [name, bbox] of Object.entries(bboxes)) {
179
+ if (name === '_meta') continue;
180
+
181
+ if (bbox.points && bbox.points.length > 0) {
182
+ if (x >= bbox.x0 - SCATTER_THRESHOLD && x <= bbox.x1 + SCATTER_THRESHOLD &&
183
+ y >= bbox.y0 - SCATTER_THRESHOLD && y <= bbox.y1 + SCATTER_THRESHOLD) {
184
+
185
+ const elementType = bbox.element_type || 'line';
186
+ let dist;
187
+
188
+ if (elementType === 'scatter') {
189
+ dist = distanceToNearestPoint(x, y, bbox.points);
190
+ } else {
191
+ dist = distanceToLine(x, y, bbox.points);
192
+ }
193
+
194
+ if (dist < minDistance) {
195
+ minDistance = dist;
196
+ closestDataElement = name;
197
+ }
198
+ }
199
+ }
200
+ }
201
+
202
+ if (closestDataElement) {
203
+ const bbox = bboxes[closestDataElement];
204
+ const threshold = (bbox.element_type === 'scatter') ? SCATTER_THRESHOLD : PROXIMITY_THRESHOLD;
205
+ if (minDistance <= threshold) {
206
+ return closestDataElement;
207
+ }
208
+ }
209
+
210
+ // Check bbox containment for other elements
211
+ const elementMatches = [];
212
+ for (const [name, bbox] of Object.entries(bboxes)) {
213
+ if (name === '_meta') continue;
214
+
215
+ if (x >= bbox.x0 && x <= bbox.x1 && y >= bbox.y0 && y <= bbox.y1) {
216
+ const area = (bbox.x1 - bbox.x0) * (bbox.y1 - bbox.y0);
217
+ const isPanel = bbox.is_panel || name.endsWith('_panel');
218
+
219
+ if (!isPanel) {
220
+ elementMatches.push({name, area, bbox});
221
+ }
222
+ }
223
+ }
224
+
225
+ if (elementMatches.length > 0) {
226
+ elementMatches.sort((a, b) => a.area - b.area);
227
+ return elementMatches[0].name;
228
+ }
229
+
230
+ return null;
231
+ }
232
+
233
+ // ============================================================================
234
+ // Distance Calculations
235
+ // ============================================================================
236
+ function distanceToNearestPoint(px, py, points) {
237
+ // Find distance to nearest point in scatter
238
+ if (!Array.isArray(points) || points.length === 0) return Infinity;
239
+ let minDist = Infinity;
240
+ for (const pt of points) {
241
+ if (!Array.isArray(pt) || pt.length < 2) continue;
242
+ const [x, y] = pt;
243
+ const dist = Math.sqrt((px - x) ** 2 + (py - y) ** 2);
244
+ if (dist < minDist) minDist = dist;
245
+ }
246
+ return minDist;
247
+ }
248
+
249
+ function distanceToLine(px, py, points) {
250
+ if (!Array.isArray(points) || points.length < 2) return Infinity;
251
+ let minDist = Infinity;
252
+ for (let i = 0; i < points.length - 1; i++) {
253
+ const pt1 = points[i];
254
+ const pt2 = points[i + 1];
255
+ if (!Array.isArray(pt1) || pt1.length < 2) continue;
256
+ if (!Array.isArray(pt2) || pt2.length < 2) continue;
257
+ const [x1, y1] = pt1;
258
+ const [x2, y2] = pt2;
259
+ const dist = distanceToSegment(px, py, x1, y1, x2, y2);
260
+ if (dist < minDist) minDist = dist;
261
+ }
262
+ return minDist;
263
+ }
264
+
265
+ function distanceToSegment(px, py, x1, y1, x2, y2) {
266
+ const dx = x2 - x1;
267
+ const dy = y2 - y1;
268
+ const lenSq = dx * dx + dy * dy;
269
+
270
+ if (lenSq === 0) {
271
+ return Math.sqrt((px - x1) ** 2 + (py - y1) ** 2);
272
+ }
273
+
274
+ let t = ((px - x1) * dx + (py - y1) * dy) / lenSq;
275
+ t = Math.max(0, Math.min(1, t));
276
+
277
+ const projX = x1 + t * dx;
278
+ const projY = y1 + t * dy;
279
+
280
+ return Math.sqrt((px - projX) ** 2 + (py - projY) ** 2);
281
+ }
282
+
283
+ // ============================================================================
284
+ // Polygon Test
285
+ // ============================================================================
286
+ function pointInPolygon(px, py, polygon) {
287
+ if (!Array.isArray(polygon) || polygon.length < 3) return false;
288
+
289
+ let inside = false;
290
+ for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
291
+ const ptI = polygon[i];
292
+ const ptJ = polygon[j];
293
+ if (!Array.isArray(ptI) || ptI.length < 2) continue;
294
+ if (!Array.isArray(ptJ) || ptJ.length < 2) continue;
295
+ const [xi, yi] = ptI;
296
+ const [xj, yj] = ptJ;
297
+
298
+ if (((yi > py) !== (yj > py)) &&
299
+ (px < (xj - xi) * (py - yi) / (yj - yi) + xi)) {
300
+ inside = !inside;
301
+ }
302
+ }
303
+ return inside;
304
+ }
305
+
306
+ // ============================================================================
307
+ // Geometry Extraction
308
+ // ============================================================================
309
+ function getGeometryPoints(bbox) {
310
+ // Extract points for overlay drawing
311
+ // Returns array of [x, y] points or null
312
+
313
+ // For scatter: use points array directly
314
+ if (bbox.element_type === 'scatter' && bbox.points) {
315
+ return bbox.points;
316
+ }
317
+
318
+ // For lines: use path_simplified
319
+ if (bbox.element_type === 'line' && bbox.path_simplified) {
320
+ return bbox.path_simplified;
321
+ }
322
+
323
+ // For fills/polygons: use polygon
324
+ if (bbox.polygon) {
325
+ return bbox.polygon;
326
+ }
327
+
328
+ return null;
329
+ }
330
+
331
+ // ============================================================================
332
+ // Axes Coordinate Transformation (for future use with geometry_px)
333
+ // ============================================================================
334
+ function axesLocalToImage(axLocalX, axLocalY, axesBbox) {
335
+ // axesBbox has: x, y, width, height in figure pixel coordinates
336
+ // The local editor uses tight layout which shifts coordinates
337
+ // For now we use the existing image coordinates from bboxes
338
+ return {x: axLocalX, y: axLocalY};
339
+ }
@@ -0,0 +1,286 @@
1
+ /**
2
+ * Element Dragging
3
+ * Handles dragging of legends and panel letters (maintains scientific rigor)
4
+ */
5
+
6
+ // ============================================================================
7
+ // Draggable Element Check
8
+ // ============================================================================
9
+ function isDraggableElement(elementName, bboxes) {
10
+ if (!elementName || !bboxes) return false;
11
+
12
+ // Whitelist: ONLY these element types are draggable
13
+ const DRAGGABLE_TYPES = ['legend', 'panel_letter'];
14
+
15
+ // Check by element_type in bbox info
16
+ const info = bboxes[elementName];
17
+ if (info && DRAGGABLE_TYPES.includes(info.element_type)) {
18
+ return true;
19
+ }
20
+
21
+ // Check by naming convention (strict match)
22
+ if (elementName.match(/_legend$/)) return true;
23
+ if (elementName.match(/_panel_letter_[A-Z]$/)) return true;
24
+
25
+ // Everything else is NOT draggable (data integrity)
26
+ return false;
27
+ }
28
+
29
+ // ============================================================================
30
+ // Element Drag Start
31
+ // ============================================================================
32
+ function startElementDrag(e, elementName, panelName, img, bboxes) {
33
+ const info = bboxes[elementName] || {};
34
+ const elementType = info.element_type || (elementName.includes('legend') ? 'legend' : 'panel_letter');
35
+
36
+ // Extract ax_id from element name (e.g., "ax_00_legend" -> "ax_00")
37
+ const axId = elementName.split('_').slice(0, 2).join('_');
38
+
39
+ // Get axes bbox for constraining drag
40
+ const axesBbox = bboxes[`${axId}_panel`] || null;
41
+
42
+ elementDragState = {
43
+ element: elementName,
44
+ panelName: panelName,
45
+ elementType: elementType,
46
+ axId: axId,
47
+ axesBbox: axesBbox,
48
+ bboxes: bboxes,
49
+ img: img,
50
+ startMouseX: e.clientX,
51
+ startMouseY: e.clientY,
52
+ startBbox: {...info},
53
+ };
54
+
55
+ // Show snap guide overlay
56
+ showSnapGuides(img, axesBbox, bboxes);
57
+
58
+ document.addEventListener('mousemove', onElementDrag);
59
+ document.addEventListener('mouseup', stopElementDrag);
60
+
61
+ e.preventDefault();
62
+ e.stopPropagation();
63
+ }
64
+
65
+ // ============================================================================
66
+ // Element Drag Move
67
+ // ============================================================================
68
+ function onElementDrag(e) {
69
+ if (!elementDragState) return;
70
+
71
+ const {img, bboxes, element, axId, axesBbox, startBbox, startMouseX, startMouseY} = elementDragState;
72
+ if (!img) return;
73
+
74
+ const rect = img.getBoundingClientRect();
75
+ const dims = getObjectFitContainDimensions(img);
76
+
77
+ // Calculate delta in image coordinates
78
+ const deltaX = e.clientX - startMouseX;
79
+ const deltaY = e.clientY - startMouseY;
80
+
81
+ // Convert to image pixel coordinates
82
+ const scaleX = dims.displayWidth / rect.width;
83
+ const scaleY = dims.displayHeight / rect.height;
84
+ const imgSize = bboxes._meta?.imgSize || {width: dims.displayWidth, height: dims.displayHeight};
85
+ const imgDeltaX = deltaX * scaleX * imgSize.width / dims.displayWidth;
86
+ const imgDeltaY = deltaY * scaleY * imgSize.height / dims.displayHeight;
87
+
88
+ // Update bbox position (for visual feedback)
89
+ if (bboxes[element]) {
90
+ const newX0 = startBbox.x0 + imgDeltaX;
91
+ const newY0 = startBbox.y0 + imgDeltaY;
92
+ bboxes[element].x0 = newX0;
93
+ bboxes[element].y0 = newY0;
94
+ bboxes[element].x1 = newX0 + (startBbox.x1 - startBbox.x0);
95
+ bboxes[element].y1 = newY0 + (startBbox.y1 - startBbox.y0);
96
+ }
97
+
98
+ // Calculate normalized axes position (0-1)
99
+ if (axesBbox) {
100
+ const axesWidth = axesBbox.x1 - axesBbox.x0;
101
+ const axesHeight = axesBbox.y1 - axesBbox.y0;
102
+ const elemCenterX = (bboxes[element].x0 + bboxes[element].x1) / 2;
103
+ const elemCenterY = (bboxes[element].y0 + bboxes[element].y1) / 2;
104
+ const normX = (elemCenterX - axesBbox.x0) / axesWidth;
105
+ const normY = 1 - (elemCenterY - axesBbox.y0) / axesHeight; // Flip Y
106
+
107
+ // Update snap guide highlighting
108
+ updateSnapHighlight(normX, normY);
109
+
110
+ // Show position indicator
111
+ showElementPositionIndicator(element, normX, normY);
112
+ }
113
+
114
+ // Redraw overlay
115
+ const overlay = img.parentElement?.querySelector('svg.panel-card-overlay');
116
+ if (overlay) {
117
+ const panelCache = panelBboxesCache[elementDragState.panelName];
118
+ if (panelCache) {
119
+ updatePanelOverlay(overlay, bboxes, panelCache.imgSize, rect.width, rect.height, element, element, img);
120
+ }
121
+ }
122
+ }
123
+
124
+ // ============================================================================
125
+ // Element Drag Stop
126
+ // ============================================================================
127
+ function stopElementDrag() {
128
+ if (!elementDragState) return;
129
+
130
+ const {element, panelName, elementType, axId, bboxes, axesBbox} = elementDragState;
131
+
132
+ // Calculate final normalized position
133
+ let finalPosition = null;
134
+ let snapName = null;
135
+
136
+ if (axesBbox && bboxes[element]) {
137
+ const axesWidth = axesBbox.x1 - axesBbox.x0;
138
+ const axesHeight = axesBbox.y1 - axesBbox.y0;
139
+ const elemCenterX = (bboxes[element].x0 + bboxes[element].x1) / 2;
140
+ const elemCenterY = (bboxes[element].y0 + bboxes[element].y1) / 2;
141
+ const normX = (elemCenterX - axesBbox.x0) / axesWidth;
142
+ const normY = 1 - (elemCenterY - axesBbox.y0) / axesHeight;
143
+
144
+ // Check for snap to named position
145
+ snapName = findNearestSnapPosition(normX, normY);
146
+ finalPosition = snapName ? SNAP_POSITIONS[snapName] : {x: normX, y: normY};
147
+ }
148
+
149
+ // Hide snap guides
150
+ hideSnapGuides();
151
+ hideElementPositionIndicator();
152
+
153
+ // Save position to server
154
+ if (finalPosition) {
155
+ updateElementPosition(panelName, element, finalPosition);
156
+ }
157
+
158
+ document.removeEventListener('mousemove', onElementDrag);
159
+ document.removeEventListener('mouseup', stopElementDrag);
160
+ elementDragState = null;
161
+ }
162
+
163
+ // ============================================================================
164
+ // Snap Position Finding
165
+ // ============================================================================
166
+ function findNearestSnapPosition(normX, normY, threshold = 0.08) {
167
+ let nearest = null;
168
+ let minDist = Infinity;
169
+
170
+ for (const [name, pos] of Object.entries(SNAP_POSITIONS)) {
171
+ const dist = Math.sqrt(Math.pow(normX - pos.x, 2) + Math.pow(normY - pos.y, 2));
172
+ if (dist < threshold && dist < minDist) {
173
+ minDist = dist;
174
+ nearest = name;
175
+ }
176
+ }
177
+ return nearest;
178
+ }
179
+
180
+ // ============================================================================
181
+ // Snap Guides UI
182
+ // ============================================================================
183
+ function showSnapGuides(img, axesBbox, bboxes) {
184
+ if (!img || !axesBbox) return;
185
+
186
+ const container = img.parentElement;
187
+ if (!container) return;
188
+
189
+ // Remove existing guides
190
+ container.querySelectorAll('.snap-guide').forEach(el => el.remove());
191
+
192
+ const rect = img.getBoundingClientRect();
193
+ const dims = getObjectFitContainDimensions(img);
194
+ const imgSize = bboxes._meta?.imgSize || {width: dims.displayWidth, height: dims.displayHeight};
195
+
196
+ // Scale factors
197
+ const scaleX = dims.displayWidth / imgSize.width;
198
+ const scaleY = dims.displayHeight / imgSize.height;
199
+
200
+ // Create snap points
201
+ for (const [name, pos] of Object.entries(SNAP_POSITIONS)) {
202
+ const axesWidth = axesBbox.x1 - axesBbox.x0;
203
+ const axesHeight = axesBbox.y1 - axesBbox.y0;
204
+
205
+ // Calculate pixel position
206
+ const imgX = axesBbox.x0 + pos.x * axesWidth;
207
+ const imgY = axesBbox.y0 + (1 - pos.y) * axesHeight;
208
+
209
+ const displayX = dims.offsetX + imgX * scaleX;
210
+ const displayY = dims.offsetY + imgY * scaleY;
211
+
212
+ const guide = document.createElement('div');
213
+ guide.className = 'snap-guide';
214
+ guide.dataset.snapName = name;
215
+ guide.style.cssText = `
216
+ position: absolute;
217
+ left: ${displayX - 6}px;
218
+ top: ${displayY - 6}px;
219
+ width: 12px;
220
+ height: 12px;
221
+ border: 2px dashed rgba(100, 149, 237, 0.6);
222
+ border-radius: 50%;
223
+ pointer-events: none;
224
+ z-index: 50;
225
+ transition: all 0.15s ease;
226
+ `;
227
+ container.appendChild(guide);
228
+ }
229
+ }
230
+
231
+ function updateSnapHighlight(normX, normY) {
232
+ const threshold = 0.08;
233
+ document.querySelectorAll('.snap-guide').forEach(guide => {
234
+ const name = guide.dataset.snapName;
235
+ const pos = SNAP_POSITIONS[name];
236
+ const dist = Math.sqrt(Math.pow(normX - pos.x, 2) + Math.pow(normY - pos.y, 2));
237
+ if (dist < threshold) {
238
+ guide.style.borderColor = 'rgba(76, 175, 80, 0.9)';
239
+ guide.style.backgroundColor = 'rgba(76, 175, 80, 0.3)';
240
+ guide.style.transform = 'scale(1.5)';
241
+ } else {
242
+ guide.style.borderColor = 'rgba(100, 149, 237, 0.6)';
243
+ guide.style.backgroundColor = 'transparent';
244
+ guide.style.transform = 'scale(1)';
245
+ }
246
+ });
247
+ }
248
+
249
+ function hideSnapGuides() {
250
+ document.querySelectorAll('.snap-guide').forEach(el => el.remove());
251
+ }
252
+
253
+ // ============================================================================
254
+ // Position Indicator
255
+ // ============================================================================
256
+ function showElementPositionIndicator(element, normX, normY) {
257
+ let indicator = document.getElementById('element-position-indicator');
258
+ if (!indicator) {
259
+ indicator = document.createElement('div');
260
+ indicator.id = 'element-position-indicator';
261
+ indicator.style.cssText = `
262
+ position: fixed;
263
+ bottom: 60px;
264
+ right: 20px;
265
+ background: rgba(0, 0, 0, 0.85);
266
+ color: white;
267
+ padding: 6px 10px;
268
+ border-radius: 4px;
269
+ font-family: monospace;
270
+ font-size: 11px;
271
+ z-index: 10001;
272
+ pointer-events: none;
273
+ `;
274
+ document.body.appendChild(indicator);
275
+ }
276
+
277
+ indicator.textContent = `${element}: (${normX.toFixed(2)}, ${normY.toFixed(2)})`;
278
+ indicator.style.display = 'block';
279
+ }
280
+
281
+ function hideElementPositionIndicator() {
282
+ const indicator = document.getElementById('element-position-indicator');
283
+ if (indicator) {
284
+ indicator.style.display = 'none';
285
+ }
286
+ }