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