figrecipe 0.6.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.
- figrecipe/__init__.py +106 -973
- figrecipe/_api/__init__.py +48 -0
- figrecipe/_api/_extract.py +108 -0
- figrecipe/_api/_notebook.py +61 -0
- figrecipe/_api/_panel.py +46 -0
- figrecipe/_api/_save.py +191 -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/_dev/__init__.py +2 -93
- figrecipe/_dev/_plotters.py +76 -0
- figrecipe/_dev/_run_demos.py +56 -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 +57 -9
- 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 +256 -0
- figrecipe/_editor/_bbox/_extract_axes.py +370 -0
- figrecipe/_editor/_bbox/_extract_text.py +342 -0
- figrecipe/_editor/_bbox/_lines.py +173 -0
- figrecipe/_editor/_bbox/_transforms.py +146 -0
- figrecipe/_editor/_flask_app.py +68 -1039
- figrecipe/_editor/_helpers.py +242 -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 +137 -0
- figrecipe/_editor/_hitmap/_restore.py +154 -0
- figrecipe/_editor/_hitmap_main.py +182 -0
- figrecipe/_editor/_preferences.py +135 -0
- figrecipe/_editor/_render_overrides.py +480 -0
- figrecipe/_editor/_renderer.py +35 -185
- figrecipe/_editor/_routes_axis.py +453 -0
- figrecipe/_editor/_routes_core.py +284 -0
- figrecipe/_editor/_routes_element.py +317 -0
- figrecipe/_editor/_routes_style.py +223 -0
- figrecipe/_editor/_templates/__init__.py +78 -1
- figrecipe/_editor/_templates/_html.py +109 -13
- figrecipe/_editor/_templates/_scripts/__init__.py +120 -0
- figrecipe/_editor/_templates/_scripts/_api.py +228 -0
- figrecipe/_editor/_templates/_scripts/_colors.py +485 -0
- figrecipe/_editor/_templates/_scripts/_core.py +436 -0
- figrecipe/_editor/_templates/_scripts/_debug_snapshot.py +186 -0
- figrecipe/_editor/_templates/_scripts/_element_editor.py +310 -0
- figrecipe/_editor/_templates/_scripts/_files.py +195 -0
- figrecipe/_editor/_templates/_scripts/_hitmap.py +509 -0
- figrecipe/_editor/_templates/_scripts/_inspector.py +315 -0
- figrecipe/_editor/_templates/_scripts/_labels.py +464 -0
- figrecipe/_editor/_templates/_scripts/_legend_drag.py +265 -0
- figrecipe/_editor/_templates/_scripts/_modals.py +226 -0
- figrecipe/_editor/_templates/_scripts/_overlays.py +292 -0
- figrecipe/_editor/_templates/_scripts/_panel_drag.py +334 -0
- figrecipe/_editor/_templates/_scripts/_panel_position.py +279 -0
- figrecipe/_editor/_templates/_scripts/_selection.py +237 -0
- figrecipe/_editor/_templates/_scripts/_tabs.py +89 -0
- figrecipe/_editor/_templates/_scripts/_view_mode.py +107 -0
- figrecipe/_editor/_templates/_scripts/_zoom.py +179 -0
- figrecipe/_editor/_templates/_styles/__init__.py +69 -0
- figrecipe/_editor/_templates/_styles/_base.py +64 -0
- figrecipe/_editor/_templates/_styles/_buttons.py +206 -0
- figrecipe/_editor/_templates/_styles/_color_input.py +123 -0
- figrecipe/_editor/_templates/_styles/_controls.py +265 -0
- figrecipe/_editor/_templates/_styles/_dynamic_props.py +144 -0
- figrecipe/_editor/_templates/_styles/_forms.py +126 -0
- figrecipe/_editor/_templates/_styles/_hitmap.py +184 -0
- figrecipe/_editor/_templates/_styles/_inspector.py +90 -0
- figrecipe/_editor/_templates/_styles/_labels.py +118 -0
- figrecipe/_editor/_templates/_styles/_modals.py +98 -0
- figrecipe/_editor/_templates/_styles/_overlays.py +130 -0
- figrecipe/_editor/_templates/_styles/_preview.py +225 -0
- figrecipe/_editor/_templates/_styles/_selection.py +73 -0
- figrecipe/_params/_DECORATION_METHODS.py +6 -0
- figrecipe/_recorder.py +35 -106
- figrecipe/_recorder_utils.py +124 -0
- figrecipe/_reproducer/__init__.py +18 -0
- figrecipe/_reproducer/_core.py +498 -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/_wrappers/_axes.py +119 -910
- figrecipe/_wrappers/_axes_helpers.py +136 -0
- figrecipe/_wrappers/_axes_plots.py +418 -0
- figrecipe/_wrappers/_axes_seaborn.py +157 -0
- figrecipe/_wrappers/_figure.py +162 -0
- figrecipe/_wrappers/_panel_labels.py +127 -0
- figrecipe/_wrappers/_plot_helpers.py +143 -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 +32 -478
- 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 +29 -24
- {figrecipe-0.6.0.dist-info → figrecipe-0.7.4.dist-info}/METADATA +37 -2
- figrecipe-0.7.4.dist-info/RECORD +188 -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/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.7.4.dist-info}/WHEEL +0 -0
- {figrecipe-0.6.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
|