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.
- figrecipe/__init__.py +161 -1030
- figrecipe/__main__.py +12 -0
- figrecipe/_api/__init__.py +48 -0
- figrecipe/_api/_extract.py +108 -0
- figrecipe/_api/_notebook.py +61 -0
- figrecipe/_api/_panel.py +113 -0
- figrecipe/_api/_save.py +287 -0
- figrecipe/_api/_seaborn_proxy.py +34 -0
- figrecipe/_api/_style_manager.py +153 -0
- figrecipe/_api/_subplots.py +333 -0
- figrecipe/_api/_validate.py +82 -0
- figrecipe/_cli/__init__.py +7 -0
- figrecipe/_cli/_compose.py +87 -0
- figrecipe/_cli/_convert.py +117 -0
- figrecipe/_cli/_crop.py +82 -0
- figrecipe/_cli/_edit.py +70 -0
- figrecipe/_cli/_extract.py +128 -0
- figrecipe/_cli/_fonts.py +47 -0
- figrecipe/_cli/_info.py +67 -0
- figrecipe/_cli/_main.py +58 -0
- figrecipe/_cli/_reproduce.py +79 -0
- figrecipe/_cli/_style.py +77 -0
- figrecipe/_cli/_validate.py +66 -0
- figrecipe/_cli/_version.py +50 -0
- figrecipe/_composition/__init__.py +32 -0
- figrecipe/_composition/_alignment.py +452 -0
- figrecipe/_composition/_compose.py +179 -0
- figrecipe/_composition/_import_axes.py +127 -0
- figrecipe/_composition/_visibility.py +125 -0
- figrecipe/_dev/__init__.py +4 -93
- figrecipe/_dev/_plotters.py +76 -0
- figrecipe/_dev/_run_demos.py +56 -0
- figrecipe/_dev/browser/__init__.py +69 -0
- figrecipe/_dev/browser/_audio.py +240 -0
- figrecipe/_dev/browser/_caption.py +356 -0
- figrecipe/_dev/browser/_click_effect.py +146 -0
- figrecipe/_dev/browser/_cursor.py +196 -0
- figrecipe/_dev/browser/_highlight.py +105 -0
- figrecipe/_dev/browser/_narration.py +237 -0
- figrecipe/_dev/browser/_recorder.py +446 -0
- figrecipe/_dev/browser/_utils.py +178 -0
- figrecipe/_dev/browser/_video_trim/__init__.py +152 -0
- figrecipe/_dev/browser/_video_trim/_detection.py +223 -0
- figrecipe/_dev/browser/_video_trim/_markers.py +140 -0
- figrecipe/_dev/demo_plotters/__init__.py +35 -166
- figrecipe/_dev/demo_plotters/_categories.py +81 -0
- figrecipe/_dev/demo_plotters/_figure_creators.py +119 -0
- figrecipe/_dev/demo_plotters/_helpers.py +31 -0
- figrecipe/_dev/demo_plotters/_registry.py +50 -0
- figrecipe/_dev/demo_plotters/bar_categorical/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/contour_surface/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/distribution/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/image_matrix/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/line_curve/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/{plot_plot.py → line_curve/plot_plot.py} +3 -2
- figrecipe/_dev/demo_plotters/scatter_points/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/special/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/{plot_pie.py → special/plot_pie.py} +5 -1
- figrecipe/_dev/demo_plotters/spectral_signal/__init__.py +4 -0
- figrecipe/_dev/demo_plotters/vector_flow/__init__.py +4 -0
- figrecipe/_editor/__init__.py +61 -13
- figrecipe/_editor/_bbox/__init__.py +43 -0
- figrecipe/_editor/_bbox/_collections.py +177 -0
- figrecipe/_editor/_bbox/_elements.py +159 -0
- figrecipe/_editor/_bbox/_extract.py +402 -0
- figrecipe/_editor/_bbox/_extract_axes.py +370 -0
- figrecipe/_editor/_bbox/_extract_text.py +466 -0
- figrecipe/_editor/_bbox/_lines.py +173 -0
- figrecipe/_editor/_bbox/_transforms.py +146 -0
- figrecipe/_editor/_call_overrides.py +183 -0
- figrecipe/_editor/_datatable_plot_handlers.py +249 -0
- figrecipe/_editor/_figure_layout.py +211 -0
- figrecipe/_editor/_flask_app.py +200 -1030
- figrecipe/_editor/_helpers.py +251 -0
- figrecipe/_editor/_hitmap/__init__.py +76 -0
- figrecipe/_editor/_hitmap/_artists/__init__.py +21 -0
- figrecipe/_editor/_hitmap/_artists/_collections.py +345 -0
- figrecipe/_editor/_hitmap/_artists/_images.py +68 -0
- figrecipe/_editor/_hitmap/_artists/_lines.py +107 -0
- figrecipe/_editor/_hitmap/_artists/_patches.py +163 -0
- figrecipe/_editor/_hitmap/_artists/_text.py +190 -0
- figrecipe/_editor/_hitmap/_colors.py +181 -0
- figrecipe/_editor/_hitmap/_detect.py +194 -0
- figrecipe/_editor/_hitmap/_restore.py +154 -0
- figrecipe/_editor/_hitmap_main.py +182 -0
- figrecipe/_editor/_overrides.py +4 -1
- figrecipe/_editor/_plot_types_registry.py +190 -0
- figrecipe/_editor/_preferences.py +135 -0
- figrecipe/_editor/_render_overrides.py +507 -0
- figrecipe/_editor/_renderer.py +81 -186
- figrecipe/_editor/_routes_annotation.py +114 -0
- figrecipe/_editor/_routes_axis.py +482 -0
- figrecipe/_editor/_routes_captions.py +130 -0
- figrecipe/_editor/_routes_composition.py +270 -0
- figrecipe/_editor/_routes_core.py +126 -0
- figrecipe/_editor/_routes_datatable.py +364 -0
- figrecipe/_editor/_routes_element.py +335 -0
- figrecipe/_editor/_routes_files.py +443 -0
- figrecipe/_editor/_routes_image.py +200 -0
- figrecipe/_editor/_routes_snapshot.py +94 -0
- figrecipe/_editor/_routes_style.py +243 -0
- figrecipe/_editor/_templates/__init__.py +116 -1
- figrecipe/_editor/_templates/_html.py +154 -64
- figrecipe/_editor/_templates/_html_components/__init__.py +13 -0
- figrecipe/_editor/_templates/_html_components/_composition_toolbar.py +79 -0
- figrecipe/_editor/_templates/_html_components/_file_browser.py +41 -0
- figrecipe/_editor/_templates/_html_datatable.py +92 -0
- figrecipe/_editor/_templates/_scripts/__init__.py +178 -0
- figrecipe/_editor/_templates/_scripts/_accordion.py +328 -0
- figrecipe/_editor/_templates/_scripts/_annotation_drag.py +504 -0
- figrecipe/_editor/_templates/_scripts/_api.py +228 -0
- figrecipe/_editor/_templates/_scripts/_canvas_context_menu.py +182 -0
- figrecipe/_editor/_templates/_scripts/_captions.py +231 -0
- figrecipe/_editor/_templates/_scripts/_colors.py +485 -0
- figrecipe/_editor/_templates/_scripts/_composition.py +283 -0
- figrecipe/_editor/_templates/_scripts/_core.py +493 -0
- figrecipe/_editor/_templates/_scripts/_datatable/__init__.py +59 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_cell_edit.py +97 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_clipboard.py +164 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_context_menu.py +221 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_core.py +150 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_editable.py +511 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_import.py +161 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_plot.py +261 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_selection.py +438 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_table.py +256 -0
- figrecipe/_editor/_templates/_scripts/_datatable/_tabs.py +354 -0
- figrecipe/_editor/_templates/_scripts/_debug_snapshot.py +186 -0
- figrecipe/_editor/_templates/_scripts/_element_editor.py +325 -0
- figrecipe/_editor/_templates/_scripts/_files.py +429 -0
- figrecipe/_editor/_templates/_scripts/_files_context_menu.py +240 -0
- figrecipe/_editor/_templates/_scripts/_hitmap.py +512 -0
- figrecipe/_editor/_templates/_scripts/_image_drop.py +428 -0
- figrecipe/_editor/_templates/_scripts/_inspector.py +315 -0
- figrecipe/_editor/_templates/_scripts/_labels.py +464 -0
- figrecipe/_editor/_templates/_scripts/_legend_drag.py +270 -0
- figrecipe/_editor/_templates/_scripts/_modals.py +226 -0
- figrecipe/_editor/_templates/_scripts/_multi_select.py +198 -0
- figrecipe/_editor/_templates/_scripts/_overlays.py +292 -0
- figrecipe/_editor/_templates/_scripts/_panel_drag.py +505 -0
- figrecipe/_editor/_templates/_scripts/_panel_drag_snapshot.py +33 -0
- figrecipe/_editor/_templates/_scripts/_panel_position.py +463 -0
- figrecipe/_editor/_templates/_scripts/_panel_resize.py +230 -0
- figrecipe/_editor/_templates/_scripts/_panel_snap.py +307 -0
- figrecipe/_editor/_templates/_scripts/_region_select.py +255 -0
- figrecipe/_editor/_templates/_scripts/_selection.py +244 -0
- figrecipe/_editor/_templates/_scripts/_sync.py +242 -0
- figrecipe/_editor/_templates/_scripts/_tabs.py +89 -0
- figrecipe/_editor/_templates/_scripts/_undo_redo.py +348 -0
- figrecipe/_editor/_templates/_scripts/_view_mode.py +107 -0
- figrecipe/_editor/_templates/_scripts/_zoom.py +212 -0
- figrecipe/_editor/_templates/_styles/__init__.py +78 -0
- figrecipe/_editor/_templates/_styles/_base.py +111 -0
- figrecipe/_editor/_templates/_styles/_buttons.py +327 -0
- figrecipe/_editor/_templates/_styles/_color_input.py +123 -0
- figrecipe/_editor/_templates/_styles/_composition.py +87 -0
- figrecipe/_editor/_templates/_styles/_controls.py +430 -0
- figrecipe/_editor/_templates/_styles/_datatable/__init__.py +40 -0
- figrecipe/_editor/_templates/_styles/_datatable/_editable.py +203 -0
- figrecipe/_editor/_templates/_styles/_datatable/_panel.py +268 -0
- figrecipe/_editor/_templates/_styles/_datatable/_table.py +479 -0
- figrecipe/_editor/_templates/_styles/_datatable/_toolbar.py +384 -0
- figrecipe/_editor/_templates/_styles/_datatable/_vars.py +123 -0
- figrecipe/_editor/_templates/_styles/_dynamic_props.py +144 -0
- figrecipe/_editor/_templates/_styles/_file_browser.py +466 -0
- figrecipe/_editor/_templates/_styles/_forms.py +224 -0
- figrecipe/_editor/_templates/_styles/_hitmap.py +191 -0
- figrecipe/_editor/_templates/_styles/_inspector.py +90 -0
- figrecipe/_editor/_templates/_styles/_labels.py +118 -0
- figrecipe/_editor/_templates/_styles/_modals.py +127 -0
- figrecipe/_editor/_templates/_styles/_overlays.py +130 -0
- figrecipe/_editor/_templates/_styles/_preview.py +430 -0
- figrecipe/_editor/_templates/_styles/_selection.py +73 -0
- figrecipe/_editor/_templates/_styles/_spinner.py +117 -0
- figrecipe/_editor/static/audio/click.mp3 +0 -0
- figrecipe/_editor/static/click.mp3 +0 -0
- figrecipe/_editor/static/icons/favicon.ico +0 -0
- figrecipe/_integrations/__init__.py +17 -0
- figrecipe/_integrations/_scitex_stats.py +298 -0
- figrecipe/_params/_DECORATION_METHODS.py +8 -0
- figrecipe/_recorder.py +63 -109
- figrecipe/_recorder_utils.py +124 -0
- figrecipe/_reproducer/__init__.py +18 -0
- figrecipe/_reproducer/_core.py +509 -0
- figrecipe/_reproducer/_custom_plots.py +279 -0
- figrecipe/_reproducer/_seaborn.py +100 -0
- figrecipe/_reproducer/_violin.py +186 -0
- figrecipe/_signatures/_kwargs.py +273 -0
- figrecipe/_signatures/_loader.py +21 -423
- figrecipe/_signatures/_parsing.py +147 -0
- figrecipe/_utils/__init__.py +3 -0
- figrecipe/_utils/_bundle.py +205 -0
- figrecipe/_wrappers/_axes.py +252 -895
- figrecipe/_wrappers/_axes_helpers.py +136 -0
- figrecipe/_wrappers/_axes_plots.py +418 -0
- figrecipe/_wrappers/_axes_seaborn.py +157 -0
- figrecipe/_wrappers/_caption_generator.py +218 -0
- figrecipe/_wrappers/_figure.py +188 -1
- figrecipe/_wrappers/_panel_labels.py +127 -0
- figrecipe/_wrappers/_plot_helpers.py +143 -0
- figrecipe/_wrappers/_stat_annotation.py +274 -0
- figrecipe/_wrappers/_violin_helpers.py +180 -0
- figrecipe/styles/__init__.py +8 -6
- figrecipe/styles/_dotdict.py +72 -0
- figrecipe/styles/_finalize.py +134 -0
- figrecipe/styles/_fonts.py +77 -0
- figrecipe/styles/_kwargs_converter.py +178 -0
- figrecipe/styles/_plot_styles.py +209 -0
- figrecipe/styles/_style_applier.py +42 -480
- figrecipe/styles/_style_loader.py +16 -192
- figrecipe/styles/_themes.py +151 -0
- figrecipe/styles/presets/MATPLOTLIB.yaml +2 -1
- figrecipe/styles/presets/SCITEX.yaml +40 -28
- figrecipe-0.9.0.dist-info/METADATA +427 -0
- figrecipe-0.9.0.dist-info/RECORD +277 -0
- figrecipe-0.9.0.dist-info/entry_points.txt +2 -0
- figrecipe/_editor/_bbox.py +0 -978
- figrecipe/_editor/_hitmap.py +0 -937
- figrecipe/_editor/_templates/_scripts.py +0 -2778
- figrecipe/_editor/_templates/_styles.py +0 -1326
- figrecipe/_reproducer.py +0 -975
- figrecipe-0.6.0.dist-info/METADATA +0 -394
- figrecipe-0.6.0.dist-info/RECORD +0 -90
- /figrecipe/_dev/demo_plotters/{plot_bar.py → bar_categorical/plot_bar.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_barh.py → bar_categorical/plot_barh.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_contour.py → contour_surface/plot_contour.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_contourf.py → contour_surface/plot_contourf.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_tricontour.py → contour_surface/plot_tricontour.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_tricontourf.py → contour_surface/plot_tricontourf.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_tripcolor.py → contour_surface/plot_tripcolor.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_triplot.py → contour_surface/plot_triplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_boxplot.py → distribution/plot_boxplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_ecdf.py → distribution/plot_ecdf.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_hist.py → distribution/plot_hist.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_hist2d.py → distribution/plot_hist2d.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_violinplot.py → distribution/plot_violinplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_hexbin.py → image_matrix/plot_hexbin.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_imshow.py → image_matrix/plot_imshow.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_matshow.py → image_matrix/plot_matshow.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_pcolor.py → image_matrix/plot_pcolor.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_pcolormesh.py → image_matrix/plot_pcolormesh.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_spy.py → image_matrix/plot_spy.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_errorbar.py → line_curve/plot_errorbar.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_fill.py → line_curve/plot_fill.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_fill_between.py → line_curve/plot_fill_between.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_fill_betweenx.py → line_curve/plot_fill_betweenx.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_stackplot.py → line_curve/plot_stackplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_stairs.py → line_curve/plot_stairs.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_step.py → line_curve/plot_step.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_scatter.py → scatter_points/plot_scatter.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_eventplot.py → special/plot_eventplot.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_loglog.py → special/plot_loglog.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_semilogx.py → special/plot_semilogx.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_semilogy.py → special/plot_semilogy.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_stem.py → special/plot_stem.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_acorr.py → spectral_signal/plot_acorr.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_angle_spectrum.py → spectral_signal/plot_angle_spectrum.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_cohere.py → spectral_signal/plot_cohere.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_csd.py → spectral_signal/plot_csd.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_magnitude_spectrum.py → spectral_signal/plot_magnitude_spectrum.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_phase_spectrum.py → spectral_signal/plot_phase_spectrum.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_psd.py → spectral_signal/plot_psd.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_specgram.py → spectral_signal/plot_specgram.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_xcorr.py → spectral_signal/plot_xcorr.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_barbs.py → vector_flow/plot_barbs.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_quiver.py → vector_flow/plot_quiver.py} +0 -0
- /figrecipe/_dev/demo_plotters/{plot_streamplot.py → vector_flow/plot_streamplot.py} +0 -0
- {figrecipe-0.6.0.dist-info → figrecipe-0.9.0.dist-info}/WHEEL +0 -0
- {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
|