figrecipe 0.5.0__py3-none-any.whl → 0.7.4__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 (189) hide show
  1. figrecipe/__init__.py +220 -819
  2. figrecipe/_api/__init__.py +48 -0
  3. figrecipe/_api/_extract.py +108 -0
  4. figrecipe/_api/_notebook.py +61 -0
  5. figrecipe/_api/_panel.py +46 -0
  6. figrecipe/_api/_save.py +191 -0
  7. figrecipe/_api/_seaborn_proxy.py +34 -0
  8. figrecipe/_api/_style_manager.py +153 -0
  9. figrecipe/_api/_subplots.py +333 -0
  10. figrecipe/_api/_validate.py +82 -0
  11. figrecipe/_dev/__init__.py +29 -0
  12. figrecipe/_dev/_plotters.py +76 -0
  13. figrecipe/_dev/_run_demos.py +56 -0
  14. figrecipe/_dev/demo_plotters/__init__.py +64 -0
  15. figrecipe/_dev/demo_plotters/_categories.py +81 -0
  16. figrecipe/_dev/demo_plotters/_figure_creators.py +119 -0
  17. figrecipe/_dev/demo_plotters/_helpers.py +31 -0
  18. figrecipe/_dev/demo_plotters/_registry.py +50 -0
  19. figrecipe/_dev/demo_plotters/bar_categorical/__init__.py +4 -0
  20. figrecipe/_dev/demo_plotters/bar_categorical/plot_bar.py +25 -0
  21. figrecipe/_dev/demo_plotters/bar_categorical/plot_barh.py +25 -0
  22. figrecipe/_dev/demo_plotters/contour_surface/__init__.py +4 -0
  23. figrecipe/_dev/demo_plotters/contour_surface/plot_contour.py +30 -0
  24. figrecipe/_dev/demo_plotters/contour_surface/plot_contourf.py +29 -0
  25. figrecipe/_dev/demo_plotters/contour_surface/plot_tricontour.py +28 -0
  26. figrecipe/_dev/demo_plotters/contour_surface/plot_tricontourf.py +28 -0
  27. figrecipe/_dev/demo_plotters/contour_surface/plot_tripcolor.py +29 -0
  28. figrecipe/_dev/demo_plotters/contour_surface/plot_triplot.py +25 -0
  29. figrecipe/_dev/demo_plotters/distribution/__init__.py +4 -0
  30. figrecipe/_dev/demo_plotters/distribution/plot_boxplot.py +24 -0
  31. figrecipe/_dev/demo_plotters/distribution/plot_ecdf.py +24 -0
  32. figrecipe/_dev/demo_plotters/distribution/plot_hist.py +24 -0
  33. figrecipe/_dev/demo_plotters/distribution/plot_hist2d.py +25 -0
  34. figrecipe/_dev/demo_plotters/distribution/plot_violinplot.py +25 -0
  35. figrecipe/_dev/demo_plotters/image_matrix/__init__.py +4 -0
  36. figrecipe/_dev/demo_plotters/image_matrix/plot_hexbin.py +25 -0
  37. figrecipe/_dev/demo_plotters/image_matrix/plot_imshow.py +23 -0
  38. figrecipe/_dev/demo_plotters/image_matrix/plot_matshow.py +23 -0
  39. figrecipe/_dev/demo_plotters/image_matrix/plot_pcolor.py +29 -0
  40. figrecipe/_dev/demo_plotters/image_matrix/plot_pcolormesh.py +29 -0
  41. figrecipe/_dev/demo_plotters/image_matrix/plot_spy.py +29 -0
  42. figrecipe/_dev/demo_plotters/line_curve/__init__.py +4 -0
  43. figrecipe/_dev/demo_plotters/line_curve/plot_errorbar.py +28 -0
  44. figrecipe/_dev/demo_plotters/line_curve/plot_fill.py +29 -0
  45. figrecipe/_dev/demo_plotters/line_curve/plot_fill_between.py +30 -0
  46. figrecipe/_dev/demo_plotters/line_curve/plot_fill_betweenx.py +28 -0
  47. figrecipe/_dev/demo_plotters/line_curve/plot_plot.py +28 -0
  48. figrecipe/_dev/demo_plotters/line_curve/plot_stackplot.py +29 -0
  49. figrecipe/_dev/demo_plotters/line_curve/plot_stairs.py +27 -0
  50. figrecipe/_dev/demo_plotters/line_curve/plot_step.py +27 -0
  51. figrecipe/_dev/demo_plotters/scatter_points/__init__.py +4 -0
  52. figrecipe/_dev/demo_plotters/scatter_points/plot_scatter.py +24 -0
  53. figrecipe/_dev/demo_plotters/special/__init__.py +4 -0
  54. figrecipe/_dev/demo_plotters/special/plot_eventplot.py +25 -0
  55. figrecipe/_dev/demo_plotters/special/plot_loglog.py +27 -0
  56. figrecipe/_dev/demo_plotters/special/plot_pie.py +27 -0
  57. figrecipe/_dev/demo_plotters/special/plot_semilogx.py +27 -0
  58. figrecipe/_dev/demo_plotters/special/plot_semilogy.py +27 -0
  59. figrecipe/_dev/demo_plotters/special/plot_stem.py +27 -0
  60. figrecipe/_dev/demo_plotters/spectral_signal/__init__.py +4 -0
  61. figrecipe/_dev/demo_plotters/spectral_signal/plot_acorr.py +24 -0
  62. figrecipe/_dev/demo_plotters/spectral_signal/plot_angle_spectrum.py +28 -0
  63. figrecipe/_dev/demo_plotters/spectral_signal/plot_cohere.py +29 -0
  64. figrecipe/_dev/demo_plotters/spectral_signal/plot_csd.py +29 -0
  65. figrecipe/_dev/demo_plotters/spectral_signal/plot_magnitude_spectrum.py +28 -0
  66. figrecipe/_dev/demo_plotters/spectral_signal/plot_phase_spectrum.py +28 -0
  67. figrecipe/_dev/demo_plotters/spectral_signal/plot_psd.py +29 -0
  68. figrecipe/_dev/demo_plotters/spectral_signal/plot_specgram.py +30 -0
  69. figrecipe/_dev/demo_plotters/spectral_signal/plot_xcorr.py +25 -0
  70. figrecipe/_dev/demo_plotters/vector_flow/__init__.py +4 -0
  71. figrecipe/_dev/demo_plotters/vector_flow/plot_barbs.py +30 -0
  72. figrecipe/_dev/demo_plotters/vector_flow/plot_quiver.py +30 -0
  73. figrecipe/_dev/demo_plotters/vector_flow/plot_streamplot.py +30 -0
  74. figrecipe/_editor/__init__.py +278 -0
  75. figrecipe/_editor/_bbox/__init__.py +43 -0
  76. figrecipe/_editor/_bbox/_collections.py +177 -0
  77. figrecipe/_editor/_bbox/_elements.py +159 -0
  78. figrecipe/_editor/_bbox/_extract.py +256 -0
  79. figrecipe/_editor/_bbox/_extract_axes.py +370 -0
  80. figrecipe/_editor/_bbox/_extract_text.py +342 -0
  81. figrecipe/_editor/_bbox/_lines.py +173 -0
  82. figrecipe/_editor/_bbox/_transforms.py +146 -0
  83. figrecipe/_editor/_flask_app.py +258 -0
  84. figrecipe/_editor/_helpers.py +242 -0
  85. figrecipe/_editor/_hitmap/__init__.py +76 -0
  86. figrecipe/_editor/_hitmap/_artists/__init__.py +21 -0
  87. figrecipe/_editor/_hitmap/_artists/_collections.py +345 -0
  88. figrecipe/_editor/_hitmap/_artists/_images.py +68 -0
  89. figrecipe/_editor/_hitmap/_artists/_lines.py +107 -0
  90. figrecipe/_editor/_hitmap/_artists/_patches.py +163 -0
  91. figrecipe/_editor/_hitmap/_artists/_text.py +190 -0
  92. figrecipe/_editor/_hitmap/_colors.py +181 -0
  93. figrecipe/_editor/_hitmap/_detect.py +137 -0
  94. figrecipe/_editor/_hitmap/_restore.py +154 -0
  95. figrecipe/_editor/_hitmap_main.py +182 -0
  96. figrecipe/_editor/_overrides.py +318 -0
  97. figrecipe/_editor/_preferences.py +135 -0
  98. figrecipe/_editor/_render_overrides.py +480 -0
  99. figrecipe/_editor/_renderer.py +199 -0
  100. figrecipe/_editor/_routes_axis.py +453 -0
  101. figrecipe/_editor/_routes_core.py +284 -0
  102. figrecipe/_editor/_routes_element.py +317 -0
  103. figrecipe/_editor/_routes_style.py +223 -0
  104. figrecipe/_editor/_templates/__init__.py +152 -0
  105. figrecipe/_editor/_templates/_html.py +502 -0
  106. figrecipe/_editor/_templates/_scripts/__init__.py +120 -0
  107. figrecipe/_editor/_templates/_scripts/_api.py +228 -0
  108. figrecipe/_editor/_templates/_scripts/_colors.py +485 -0
  109. figrecipe/_editor/_templates/_scripts/_core.py +436 -0
  110. figrecipe/_editor/_templates/_scripts/_debug_snapshot.py +186 -0
  111. figrecipe/_editor/_templates/_scripts/_element_editor.py +310 -0
  112. figrecipe/_editor/_templates/_scripts/_files.py +195 -0
  113. figrecipe/_editor/_templates/_scripts/_hitmap.py +509 -0
  114. figrecipe/_editor/_templates/_scripts/_inspector.py +315 -0
  115. figrecipe/_editor/_templates/_scripts/_labels.py +464 -0
  116. figrecipe/_editor/_templates/_scripts/_legend_drag.py +265 -0
  117. figrecipe/_editor/_templates/_scripts/_modals.py +226 -0
  118. figrecipe/_editor/_templates/_scripts/_overlays.py +292 -0
  119. figrecipe/_editor/_templates/_scripts/_panel_drag.py +334 -0
  120. figrecipe/_editor/_templates/_scripts/_panel_position.py +279 -0
  121. figrecipe/_editor/_templates/_scripts/_selection.py +237 -0
  122. figrecipe/_editor/_templates/_scripts/_tabs.py +89 -0
  123. figrecipe/_editor/_templates/_scripts/_view_mode.py +107 -0
  124. figrecipe/_editor/_templates/_scripts/_zoom.py +179 -0
  125. figrecipe/_editor/_templates/_styles/__init__.py +69 -0
  126. figrecipe/_editor/_templates/_styles/_base.py +64 -0
  127. figrecipe/_editor/_templates/_styles/_buttons.py +206 -0
  128. figrecipe/_editor/_templates/_styles/_color_input.py +123 -0
  129. figrecipe/_editor/_templates/_styles/_controls.py +265 -0
  130. figrecipe/_editor/_templates/_styles/_dynamic_props.py +144 -0
  131. figrecipe/_editor/_templates/_styles/_forms.py +126 -0
  132. figrecipe/_editor/_templates/_styles/_hitmap.py +184 -0
  133. figrecipe/_editor/_templates/_styles/_inspector.py +90 -0
  134. figrecipe/_editor/_templates/_styles/_labels.py +118 -0
  135. figrecipe/_editor/_templates/_styles/_modals.py +98 -0
  136. figrecipe/_editor/_templates/_styles/_overlays.py +130 -0
  137. figrecipe/_editor/_templates/_styles/_preview.py +225 -0
  138. figrecipe/_editor/_templates/_styles/_selection.py +73 -0
  139. figrecipe/_params/_DECORATION_METHODS.py +33 -0
  140. figrecipe/_params/_PLOTTING_METHODS.py +58 -0
  141. figrecipe/_params/__init__.py +9 -0
  142. figrecipe/_recorder.py +92 -110
  143. figrecipe/_recorder_utils.py +124 -0
  144. figrecipe/_reproducer/__init__.py +18 -0
  145. figrecipe/_reproducer/_core.py +498 -0
  146. figrecipe/_reproducer/_custom_plots.py +279 -0
  147. figrecipe/_reproducer/_seaborn.py +100 -0
  148. figrecipe/_reproducer/_violin.py +186 -0
  149. figrecipe/_seaborn.py +14 -9
  150. figrecipe/_serializer.py +2 -2
  151. figrecipe/_signatures/README.md +68 -0
  152. figrecipe/_signatures/__init__.py +12 -2
  153. figrecipe/_signatures/_kwargs.py +273 -0
  154. figrecipe/_signatures/_loader.py +114 -57
  155. figrecipe/_signatures/_parsing.py +147 -0
  156. figrecipe/_utils/__init__.py +6 -4
  157. figrecipe/_utils/_crop.py +10 -4
  158. figrecipe/_utils/_image_diff.py +37 -33
  159. figrecipe/_utils/_numpy_io.py +0 -1
  160. figrecipe/_utils/_units.py +11 -3
  161. figrecipe/_validator.py +12 -3
  162. figrecipe/_wrappers/_axes.py +193 -170
  163. figrecipe/_wrappers/_axes_helpers.py +136 -0
  164. figrecipe/_wrappers/_axes_plots.py +418 -0
  165. figrecipe/_wrappers/_axes_seaborn.py +157 -0
  166. figrecipe/_wrappers/_figure.py +277 -18
  167. figrecipe/_wrappers/_panel_labels.py +127 -0
  168. figrecipe/_wrappers/_plot_helpers.py +143 -0
  169. figrecipe/_wrappers/_violin_helpers.py +180 -0
  170. figrecipe/plt.py +0 -1
  171. figrecipe/pyplot.py +2 -1
  172. figrecipe/styles/__init__.py +12 -11
  173. figrecipe/styles/_dotdict.py +72 -0
  174. figrecipe/styles/_finalize.py +134 -0
  175. figrecipe/styles/_fonts.py +77 -0
  176. figrecipe/styles/_kwargs_converter.py +178 -0
  177. figrecipe/styles/_plot_styles.py +209 -0
  178. figrecipe/styles/_style_applier.py +60 -202
  179. figrecipe/styles/_style_loader.py +73 -121
  180. figrecipe/styles/_themes.py +151 -0
  181. figrecipe/styles/presets/MATPLOTLIB.yaml +95 -0
  182. figrecipe/styles/presets/SCITEX.yaml +181 -0
  183. figrecipe-0.7.4.dist-info/METADATA +429 -0
  184. figrecipe-0.7.4.dist-info/RECORD +188 -0
  185. figrecipe/_reproducer.py +0 -358
  186. figrecipe-0.5.0.dist-info/METADATA +0 -336
  187. figrecipe-0.5.0.dist-info/RECORD +0 -26
  188. {figrecipe-0.5.0.dist-info → figrecipe-0.7.4.dist-info}/WHEEL +0 -0
  189. {figrecipe-0.5.0.dist-info → figrecipe-0.7.4.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,509 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Hitmap and selection JavaScript for the figure editor.
4
+
5
+ This module contains the JavaScript code for:
6
+ - Loading and displaying hitmap overlay
7
+ - Hit region drawing (SVG shapes for clickable elements)
8
+ - Element selection and group selection
9
+ - Hover highlighting
10
+ - Alt+Click cycling through overlapping elements
11
+ """
12
+
13
+ SCRIPTS_HITMAP = """
14
+ // ===== HITMAP AND SELECTION =====
15
+
16
+ // Load hitmap data from server
17
+ async function loadHitmap() {
18
+ try {
19
+ // Load hitmap and calls data in parallel
20
+ const [hitmapResponse, callsResponse] = await Promise.all([
21
+ fetch('/hitmap'),
22
+ fetch('/calls')
23
+ ]);
24
+ const data = await hitmapResponse.json();
25
+ callsData = await callsResponse.json();
26
+
27
+ colorMap = data.color_map;
28
+ console.log('Loaded colorMap:', Object.keys(colorMap));
29
+
30
+ // Create canvas for hitmap
31
+ const canvas = document.getElementById('hitmap-canvas');
32
+ hitmapCtx = canvas.getContext('2d', { willReadFrequently: true });
33
+
34
+ // Load hitmap image
35
+ hitmapImg = new Image();
36
+ hitmapImg.onload = function() {
37
+ canvas.width = hitmapImg.width;
38
+ canvas.height = hitmapImg.height;
39
+ hitmapCtx.drawImage(hitmapImg, 0, 0);
40
+ hitmapLoaded = true;
41
+ console.log('Hitmap loaded:', hitmapImg.width, 'x', hitmapImg.height);
42
+
43
+ // Update overlay image source
44
+ const overlay = document.getElementById('hitmap-overlay');
45
+ if (overlay) {
46
+ overlay.src = hitmapImg.src;
47
+ }
48
+ };
49
+ hitmapImg.src = 'data:image/png;base64,' + data.image;
50
+ } catch (error) {
51
+ console.error('Failed to load hitmap:', error);
52
+ }
53
+ }
54
+
55
+ // Toggle hit regions overlay visibility mode
56
+ function toggleHitmapOverlay() {
57
+ hitmapVisible = !hitmapVisible;
58
+ const overlay = document.getElementById('hitregion-overlay');
59
+ const btn = document.getElementById('btn-show-hitmap');
60
+
61
+ if (hitmapVisible) {
62
+ // Show all hit regions
63
+ overlay.classList.add('visible');
64
+ overlay.classList.remove('hover-mode');
65
+ btn.classList.add('active');
66
+ btn.textContent = 'Hide Hit Regions';
67
+ } else {
68
+ // Hover-only mode: hit regions visible only on hover
69
+ overlay.classList.remove('visible');
70
+ overlay.classList.add('hover-mode');
71
+ btn.classList.remove('active');
72
+ btn.textContent = 'Show Hit Regions';
73
+ }
74
+ // Always draw hit regions for hover detection
75
+ drawHitRegions();
76
+ }
77
+
78
+ // Draw hit region shapes from bboxes (polylines for lines, rectangles for others)
79
+ function drawHitRegions() {
80
+ const overlay = document.getElementById('hitregion-overlay');
81
+ overlay.innerHTML = '';
82
+
83
+ const img = document.getElementById('preview-image');
84
+
85
+ // Wait for image to load before drawing hit regions
86
+ if (!img.naturalWidth || !img.naturalHeight) {
87
+ console.log('Image not loaded yet, deferring hit regions draw');
88
+ return;
89
+ }
90
+
91
+ // Set SVG viewBox to match natural image size
92
+ // CSS transform on zoom-container handles all scaling
93
+ overlay.setAttribute('viewBox', `0 0 ${img.naturalWidth} ${img.naturalHeight}`);
94
+ overlay.style.width = `${img.naturalWidth}px`;
95
+ overlay.style.height = `${img.naturalHeight}px`;
96
+
97
+ // Use scale=1.0 since SVG coordinates match bbox coordinates (both in natural image pixels)
98
+ const offsetX = 0;
99
+ const offsetY = 0;
100
+ const scaleX = 1.0;
101
+ const scaleY = 1.0;
102
+
103
+ console.log('Drawing hit regions:', Object.keys(currentBboxes).length, 'elements');
104
+
105
+ // Drawing z-order: background first (lower), foreground last (higher = on top visually)
106
+ const zOrderPriority = { 'axes': 0, 'fill': 1, 'spine': 2, 'image': 3, 'contour': 3,
107
+ 'bar': 4, 'pie': 4, 'quiver': 4, 'line': 5, 'scatter': 6, 'xticks': 7, 'yticks': 7,
108
+ 'title': 8, 'xlabel': 8, 'ylabel': 8, 'legend': 9 };
109
+
110
+ // Convert to array, filter, and sort by z-order
111
+ // Include axes (panels) - they have lowest z-order so drawn first (background)
112
+ const sortedEntries = Object.entries(currentBboxes)
113
+ .filter(([key, bbox]) => key !== '_meta' && bbox && typeof bbox.x !== 'undefined')
114
+ .sort((a, b) => (zOrderPriority[a[1].type] || 5) - (zOrderPriority[b[1].type] || 5));
115
+
116
+ // Draw shapes for each bbox (in z-order)
117
+ for (const [key, bbox] of sortedEntries) {
118
+ const colorMapInfo = (colorMap && colorMap[key]) || {};
119
+ const originalColor = colorMapInfo.original_color || bbox.original_color;
120
+
121
+ // Create group for shape and label
122
+ const group = document.createElementNS('http://www.w3.org/2000/svg', 'g');
123
+ group.setAttribute('class', 'hitregion-group');
124
+ group.setAttribute('data-key', key);
125
+
126
+ let shape;
127
+ let labelX, labelY;
128
+
129
+ // Use polyline for lines with points, circles for scatter, rectangle for others
130
+ if (bbox.type === 'line' && bbox.points && bbox.points.length > 1) {
131
+ shape = _createPolylineShape(bbox, key, originalColor, offsetX, offsetY, scaleX, scaleY);
132
+ const firstPt = bbox.points[0];
133
+ labelX = offsetX + firstPt[0] * scaleX + 5;
134
+ labelY = offsetY + firstPt[1] * scaleY - 5;
135
+ } else if (bbox.type === 'scatter' && bbox.points && bbox.points.length > 0) {
136
+ shape = _createScatterShape(bbox, key, originalColor, offsetX, offsetY, scaleX, scaleY);
137
+ const firstPt = bbox.points[0];
138
+ labelX = offsetX + firstPt[0] * scaleX + 5;
139
+ labelY = offsetY + firstPt[1] * scaleY - 5;
140
+ } else {
141
+ const result = _createRectShape(bbox, key, originalColor, offsetX, offsetY, scaleX, scaleY);
142
+ shape = result.shape;
143
+ labelX = result.labelX;
144
+ labelY = result.labelY;
145
+ }
146
+
147
+ // Add hover and click handlers
148
+ const callId = colorMapInfo.call_id || colorMapInfo.label || bbox.label;
149
+ const enrichedBbox = { ...bbox, original_color: originalColor, call_id: callId };
150
+ shape.addEventListener('mouseenter', () => handleHitRegionHover(key, enrichedBbox));
151
+ shape.addEventListener('mouseleave', () => handleHitRegionLeave());
152
+ shape.addEventListener('click', (e) => handleHitRegionClick(e, key, enrichedBbox));
153
+
154
+ // Add mousedown for drag (legend or panel)
155
+ shape.addEventListener('mousedown', (e) => {
156
+ if (e.button !== 0 || e.ctrlKey || e.metaKey || e.altKey) return;
157
+ if (bbox.type === 'legend' && typeof startLegendDrag === 'function') { startLegendDrag(e, key); return; }
158
+ if (typeof handlePanelDragStart === 'function') handlePanelDragStart(e);
159
+ });
160
+
161
+ group.appendChild(shape);
162
+
163
+ // Create label
164
+ const elemType = colorMapInfo.type || bbox.type || 'element';
165
+ const elemLabel = colorMapInfo.label || bbox.label || key;
166
+ const label = document.createElementNS('http://www.w3.org/2000/svg', 'text');
167
+ label.setAttribute('x', labelX);
168
+ label.setAttribute('y', labelY);
169
+ label.setAttribute('class', 'hitregion-label');
170
+ label.textContent = `${elemType}: ${elemLabel}`;
171
+ group.appendChild(label);
172
+
173
+ overlay.appendChild(group);
174
+ }
175
+ }
176
+
177
+ // Helper: Create polyline shape for lines
178
+ function _createPolylineShape(bbox, key, originalColor, offsetX, offsetY, scaleX, scaleY) {
179
+ const points = bbox.points.map(pt => {
180
+ const x = offsetX + pt[0] * scaleX;
181
+ const y = offsetY + pt[1] * scaleY;
182
+ return `${x},${y}`;
183
+ }).join(' ');
184
+
185
+ const shape = document.createElementNS('http://www.w3.org/2000/svg', 'polyline');
186
+ shape.setAttribute('points', points);
187
+ shape.setAttribute('class', 'hitregion-polyline');
188
+ shape.setAttribute('data-key', key);
189
+ if (originalColor) {
190
+ shape.style.setProperty('--element-color', originalColor);
191
+ }
192
+ return shape;
193
+ }
194
+
195
+ // Helper: Create scatter circles group
196
+ function _createScatterShape(bbox, key, originalColor, offsetX, offsetY, scaleX, scaleY) {
197
+ const shape = document.createElementNS('http://www.w3.org/2000/svg', 'g');
198
+ shape.setAttribute('class', 'scatter-group');
199
+ shape.setAttribute('data-key', key);
200
+ if (originalColor) {
201
+ shape.style.setProperty('--element-color', originalColor);
202
+ }
203
+
204
+ const hitRadius = 5;
205
+ const allCircles = [];
206
+
207
+ bbox.points.forEach((pt, idx) => {
208
+ const cx = offsetX + pt[0] * scaleX;
209
+ const cy = offsetY + pt[1] * scaleY;
210
+
211
+ const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
212
+ circle.setAttribute('cx', cx);
213
+ circle.setAttribute('cy', cy);
214
+ circle.setAttribute('r', hitRadius);
215
+ circle.setAttribute('class', 'hitregion-circle');
216
+ circle.setAttribute('data-key', key);
217
+ circle.setAttribute('data-point-index', idx);
218
+
219
+ allCircles.push(circle);
220
+ shape.appendChild(circle);
221
+ });
222
+
223
+ // Add event handlers to scatter group
224
+ shape.addEventListener('mouseenter', () => {
225
+ handleHitRegionHover(key, bbox);
226
+ allCircles.forEach(c => c.classList.add('hovered'));
227
+ shape.classList.add('hovered');
228
+ });
229
+ shape.addEventListener('mouseleave', () => {
230
+ handleHitRegionLeave();
231
+ allCircles.forEach(c => c.classList.remove('hovered'));
232
+ shape.classList.remove('hovered');
233
+ });
234
+ shape.addEventListener('click', (e) => handleHitRegionClick(e, key, bbox));
235
+
236
+ return shape;
237
+ }
238
+
239
+ // Helper: Create rectangle shape for other elements
240
+ function _createRectShape(bbox, key, originalColor, offsetX, offsetY, scaleX, scaleY) {
241
+ let regionClass = 'hitregion-rect';
242
+ if (bbox.type === 'line' || bbox.type === 'scatter') {
243
+ regionClass += ' line-region';
244
+ } else if (['title', 'xlabel', 'ylabel', 'suptitle', 'supxlabel', 'supylabel'].includes(bbox.type)) {
245
+ regionClass += ' text-region';
246
+ } else if (bbox.type === 'legend') {
247
+ regionClass += ' legend-region';
248
+ } else if (bbox.type === 'xticks' || bbox.type === 'yticks') {
249
+ regionClass += ' tick-region';
250
+ }
251
+
252
+ const x = offsetX + bbox.x * scaleX;
253
+ const y = offsetY + bbox.y * scaleY;
254
+ const width = bbox.width * scaleX;
255
+ const height = bbox.height * scaleY;
256
+
257
+ const shape = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
258
+ shape.setAttribute('x', x);
259
+ shape.setAttribute('y', y);
260
+ shape.setAttribute('width', Math.max(width, 5));
261
+ shape.setAttribute('height', Math.max(height, 5));
262
+ shape.setAttribute('class', regionClass);
263
+ shape.setAttribute('data-key', key);
264
+ if (originalColor) {
265
+ shape.style.setProperty('--element-color', originalColor);
266
+ }
267
+
268
+ return { shape, labelX: x + 2, labelY: y - 3 };
269
+ }
270
+
271
+ // Handle hover on hit region
272
+ function handleHitRegionHover(key, bbox) {
273
+ const colorMapInfo = (colorMap && colorMap[key]) || {};
274
+ hoveredElement = { key, ...bbox, ...colorMapInfo };
275
+
276
+ const callId = colorMapInfo.call_id;
277
+ if (callId) {
278
+ const groupElements = findGroupElements(callId);
279
+ if (groupElements.length > 1) {
280
+ highlightGroupElements(groupElements.map(e => e.key));
281
+ }
282
+ }
283
+ }
284
+
285
+ // Highlight all elements in a group
286
+ function highlightGroupElements(keys) {
287
+ keys.forEach(key => {
288
+ const hitRegion = document.querySelector(`[data-key="${key}"]`);
289
+ if (hitRegion) {
290
+ hitRegion.classList.add('group-hovered');
291
+ }
292
+ });
293
+ }
294
+
295
+ // Handle leaving hit region
296
+ function handleHitRegionLeave() {
297
+ hoveredElement = null;
298
+ document.querySelectorAll('.group-hovered').forEach(el => {
299
+ el.classList.remove('group-hovered');
300
+ });
301
+ }
302
+
303
+ // Handle click on hit region with Alt+Click cycling support
304
+ function handleHitRegionClick(event, key, bbox) {
305
+ // Skip if dragging a panel (isDraggingPanel defined in _panel_drag.py)
306
+ if (typeof isDraggingPanel !== 'undefined' && isDraggingPanel) return;
307
+
308
+ event.stopPropagation();
309
+ event.preventDefault();
310
+
311
+ const colorMapInfo = (colorMap && colorMap[key]) || {};
312
+ const element = { key, ...bbox, ...colorMapInfo };
313
+
314
+ if (event.altKey) {
315
+ // Alt+Click: cycle through overlapping elements
316
+ const clickPos = { x: event.clientX, y: event.clientY };
317
+ const samePosition = lastClickPosition &&
318
+ Math.abs(lastClickPosition.x - clickPos.x) < 5 &&
319
+ Math.abs(lastClickPosition.y - clickPos.y) < 5;
320
+
321
+ if (samePosition && overlappingElements.length > 1) {
322
+ cycleIndex = (cycleIndex + 1) % overlappingElements.length;
323
+ selectElement(overlappingElements[cycleIndex]);
324
+ } else {
325
+ overlappingElements = findOverlappingElements(clickPos);
326
+ cycleIndex = 0;
327
+ lastClickPosition = clickPos;
328
+
329
+ if (overlappingElements.length > 0) {
330
+ selectElement(overlappingElements[0]);
331
+ } else {
332
+ selectElement(element);
333
+ }
334
+ }
335
+ } else {
336
+ // Normal click: select the hovered element
337
+ selectElement(element);
338
+ lastClickPosition = null;
339
+ overlappingElements = [];
340
+ cycleIndex = 0;
341
+ }
342
+ }
343
+
344
+ // Find all elements overlapping at a given screen position
345
+ function findOverlappingElements(screenPos) {
346
+ const img = document.getElementById('preview-image');
347
+ const imgRect = img.getBoundingClientRect();
348
+
349
+ const imgX = (screenPos.x - imgRect.left) * (img.naturalWidth / imgRect.width);
350
+ const imgY = (screenPos.y - imgRect.top) * (img.naturalHeight / imgRect.height);
351
+
352
+ const overlapping = [];
353
+
354
+ for (const [key, bbox] of Object.entries(currentBboxes)) {
355
+ if (key === '_meta') continue;
356
+
357
+ if (imgX >= bbox.x && imgX <= bbox.x + bbox.width &&
358
+ imgY >= bbox.y && imgY <= bbox.y + bbox.height) {
359
+ overlapping.push({ key, ...bbox });
360
+ }
361
+
362
+ // For lines with points, check proximity
363
+ if (bbox.points && bbox.points.length > 1) {
364
+ for (const pt of bbox.points) {
365
+ const dist = Math.sqrt(Math.pow(imgX - pt[0], 2) + Math.pow(imgY - pt[1], 2));
366
+ if (dist < 15) {
367
+ if (!overlapping.find(e => e.key === key)) {
368
+ overlapping.push({ key, ...bbox });
369
+ }
370
+ break;
371
+ }
372
+ }
373
+ }
374
+ }
375
+
376
+ // Click priority: smaller/precise elements first, large background last (lower = higher priority)
377
+ const clickPriority = { 'scatter': 0, 'legend': 1, 'title': 2, 'xlabel': 2, 'ylabel': 2,
378
+ 'line': 3, 'bar': 4, 'pie': 4, 'contour': 5, 'quiver': 5, 'image': 5, 'fill': 6,
379
+ 'xticks': 7, 'yticks': 7, 'spine': 8, 'axes': 9 };
380
+ overlapping.sort((a, b) => (clickPriority[a.type] ?? 5) - (clickPriority[b.type] ?? 5));
381
+
382
+ return overlapping;
383
+ }
384
+
385
+ // Update hit regions when image loads or resizes
386
+ function updateHitRegions() {
387
+ drawHitRegions();
388
+ }
389
+
390
+ // Handle click on preview image
391
+ function handlePreviewClick(event) {
392
+ const img = event.target;
393
+ const rect = img.getBoundingClientRect();
394
+
395
+ const x = event.clientX - rect.left;
396
+ const y = event.clientY - rect.top;
397
+
398
+ const scaleX = img.naturalWidth / rect.width;
399
+ const scaleY = img.naturalHeight / rect.height;
400
+ const imgX = Math.floor(x * scaleX);
401
+ const imgY = Math.floor(y * scaleY);
402
+
403
+ const element = getElementAtPosition(imgX, imgY);
404
+
405
+ if (element) {
406
+ selectElement(element);
407
+ } else {
408
+ clearSelection();
409
+ }
410
+ }
411
+
412
+ // Get element at image position using hitmap
413
+ function getElementAtPosition(imgX, imgY) {
414
+ if (!hitmapLoaded) {
415
+ return null;
416
+ }
417
+
418
+ const scaleX = hitmapImg.width / currentImgWidth;
419
+ const scaleY = hitmapImg.height / currentImgHeight;
420
+ const hitmapX = Math.floor(imgX * scaleX);
421
+ const hitmapY = Math.floor(imgY * scaleY);
422
+
423
+ try {
424
+ const pixel = hitmapCtx.getImageData(hitmapX, hitmapY, 1, 1).data;
425
+ const [r, g, b, a] = pixel;
426
+
427
+ // Skip transparent or background
428
+ if (a < 128) return null;
429
+ if (r === 26 && g === 26 && b === 26) return null;
430
+ if (r === 64 && g === 64 && b === 64) return null;
431
+
432
+ // Find element by RGB color
433
+ if (colorMap) {
434
+ for (const [key, info] of Object.entries(colorMap)) {
435
+ if (info.rgb[0] === r && info.rgb[1] === g && info.rgb[2] === b) {
436
+ return { key, ...info };
437
+ }
438
+ }
439
+ }
440
+ } catch (error) {
441
+ console.error('Hitmap pixel read error:', error);
442
+ }
443
+
444
+ // Fallback: check bboxes
445
+ for (const [key, bbox] of Object.entries(currentBboxes)) {
446
+ if (key === '_meta') continue;
447
+ if (imgX >= bbox.x && imgX <= bbox.x + bbox.width &&
448
+ imgY >= bbox.y && imgY <= bbox.y + bbox.height) {
449
+ return { key, ...bbox };
450
+ }
451
+ }
452
+
453
+ return null;
454
+ }
455
+
456
+ // Find all elements belonging to the same logical group
457
+ function findGroupElements(callId) {
458
+ if (!callId || !colorMap) return [];
459
+
460
+ const groupElements = [];
461
+ for (const [key, info] of Object.entries(colorMap)) {
462
+ if (info.call_id === callId) {
463
+ groupElements.push({ key, ...info });
464
+ }
465
+ }
466
+ return groupElements;
467
+ }
468
+
469
+ // Get representative color for a call_id group
470
+ function getGroupRepresentativeColor(callId, fallbackColor) {
471
+ if (!callId || !colorMap) return fallbackColor;
472
+
473
+ const groupElements = findGroupElements(callId);
474
+ if (groupElements.length === 0) return fallbackColor;
475
+
476
+ const firstColor = groupElements[0].original_color;
477
+ if (!firstColor) return fallbackColor;
478
+
479
+ const allSameColor = groupElements.every(el => el.original_color === firstColor);
480
+ return allSameColor ? firstColor : firstColor;
481
+ }
482
+
483
+ // Select an element (and its logical group if applicable)
484
+ function selectElement(element) {
485
+ selectedElement = element;
486
+
487
+ const callId = element.call_id || element.label;
488
+ const groupElements = findGroupElements(callId);
489
+
490
+ selectedElement.groupElements = groupElements.length > 1 ? groupElements : null;
491
+
492
+ drawSelection(element.key);
493
+ autoSwitchTab(element.type);
494
+ updateTabHints();
495
+ syncPropertiesToElement(element);
496
+
497
+ // Sync with panel position if axes type or has ax_index
498
+ if (element.type === 'axes' || element.ax_index !== undefined) {
499
+ const axIndex = element.ax_index !== undefined ? element.ax_index : getPanelIndexFromKey(element.key);
500
+ if (axIndex !== null && typeof selectPanelByIndex === 'function') {
501
+ selectPanelByIndex(axIndex);
502
+ }
503
+ }
504
+ }
505
+ """
506
+
507
+ __all__ = ["SCRIPTS_HITMAP"]
508
+
509
+ # EOF