figrecipe 0.7.4__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 +74 -76
- figrecipe/__main__.py +12 -0
- figrecipe/_api/_panel.py +67 -0
- figrecipe/_api/_save.py +100 -4
- 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 +2 -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/_editor/__init__.py +36 -36
- figrecipe/_editor/_bbox/_extract.py +155 -9
- figrecipe/_editor/_bbox/_extract_text.py +124 -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 +157 -16
- figrecipe/_editor/_helpers.py +17 -8
- figrecipe/_editor/_hitmap/_detect.py +89 -32
- figrecipe/_editor/_hitmap_main.py +4 -4
- figrecipe/_editor/_overrides.py +4 -1
- figrecipe/_editor/_plot_types_registry.py +190 -0
- figrecipe/_editor/_render_overrides.py +38 -11
- figrecipe/_editor/_renderer.py +46 -1
- figrecipe/_editor/_routes_annotation.py +114 -0
- figrecipe/_editor/_routes_axis.py +35 -6
- figrecipe/_editor/_routes_captions.py +130 -0
- figrecipe/_editor/_routes_composition.py +270 -0
- figrecipe/_editor/_routes_core.py +15 -173
- figrecipe/_editor/_routes_datatable.py +364 -0
- figrecipe/_editor/_routes_element.py +37 -19
- 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 +28 -8
- figrecipe/_editor/_templates/__init__.py +40 -2
- figrecipe/_editor/_templates/_html.py +97 -103
- 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 +58 -0
- figrecipe/_editor/_templates/_scripts/_accordion.py +328 -0
- figrecipe/_editor/_templates/_scripts/_annotation_drag.py +504 -0
- figrecipe/_editor/_templates/_scripts/_api.py +1 -1
- figrecipe/_editor/_templates/_scripts/_canvas_context_menu.py +182 -0
- figrecipe/_editor/_templates/_scripts/_captions.py +231 -0
- figrecipe/_editor/_templates/_scripts/_composition.py +283 -0
- figrecipe/_editor/_templates/_scripts/_core.py +94 -37
- 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/_element_editor.py +17 -2
- figrecipe/_editor/_templates/_scripts/_files.py +274 -40
- figrecipe/_editor/_templates/_scripts/_files_context_menu.py +240 -0
- figrecipe/_editor/_templates/_scripts/_hitmap.py +87 -84
- figrecipe/_editor/_templates/_scripts/_image_drop.py +428 -0
- figrecipe/_editor/_templates/_scripts/_legend_drag.py +5 -0
- figrecipe/_editor/_templates/_scripts/_multi_select.py +198 -0
- figrecipe/_editor/_templates/_scripts/_panel_drag.py +219 -48
- figrecipe/_editor/_templates/_scripts/_panel_drag_snapshot.py +33 -0
- figrecipe/_editor/_templates/_scripts/_panel_position.py +238 -54
- 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 +8 -1
- figrecipe/_editor/_templates/_scripts/_sync.py +242 -0
- figrecipe/_editor/_templates/_scripts/_undo_redo.py +348 -0
- figrecipe/_editor/_templates/_scripts/_zoom.py +52 -19
- figrecipe/_editor/_templates/_styles/__init__.py +9 -0
- figrecipe/_editor/_templates/_styles/_base.py +47 -0
- figrecipe/_editor/_templates/_styles/_buttons.py +127 -6
- figrecipe/_editor/_templates/_styles/_composition.py +87 -0
- figrecipe/_editor/_templates/_styles/_controls.py +168 -3
- 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 +5 -5
- figrecipe/_editor/_templates/_styles/_file_browser.py +466 -0
- figrecipe/_editor/_templates/_styles/_forms.py +98 -0
- figrecipe/_editor/_templates/_styles/_hitmap.py +7 -0
- figrecipe/_editor/_templates/_styles/_modals.py +29 -0
- figrecipe/_editor/_templates/_styles/_overlays.py +5 -5
- figrecipe/_editor/_templates/_styles/_preview.py +213 -8
- 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 +2 -0
- figrecipe/_recorder.py +28 -3
- figrecipe/_reproducer/_core.py +60 -49
- figrecipe/_utils/__init__.py +3 -0
- figrecipe/_utils/_bundle.py +205 -0
- figrecipe/_wrappers/_axes.py +150 -2
- figrecipe/_wrappers/_caption_generator.py +218 -0
- figrecipe/_wrappers/_figure.py +26 -1
- figrecipe/_wrappers/_stat_annotation.py +274 -0
- figrecipe/styles/_style_applier.py +10 -2
- figrecipe/styles/presets/SCITEX.yaml +11 -4
- {figrecipe-0.7.4.dist-info → figrecipe-0.9.0.dist-info}/METADATA +144 -146
- figrecipe-0.9.0.dist-info/RECORD +277 -0
- figrecipe-0.9.0.dist-info/entry_points.txt +2 -0
- figrecipe-0.7.4.dist-info/RECORD +0 -188
- {figrecipe-0.7.4.dist-info → figrecipe-0.9.0.dist-info}/WHEEL +0 -0
- {figrecipe-0.7.4.dist-info → figrecipe-0.9.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Datatable table rendering JavaScript with smart cell truncation."""
|
|
4
|
+
|
|
5
|
+
JS_DATATABLE_TABLE = """
|
|
6
|
+
// ============================================================================
|
|
7
|
+
// Render Datatable (with span-wrapped cells for smart truncation)
|
|
8
|
+
// ============================================================================
|
|
9
|
+
function renderDatatable() {
|
|
10
|
+
const content = document.getElementById('datatable-content');
|
|
11
|
+
if (!content || !datatableData) return;
|
|
12
|
+
|
|
13
|
+
const { columns, rows } = datatableData;
|
|
14
|
+
|
|
15
|
+
// Build table HTML with span-wrapped content (vis_app pattern)
|
|
16
|
+
let html = '<table class="datatable-table" tabindex="0">';
|
|
17
|
+
|
|
18
|
+
// Header row
|
|
19
|
+
html += '<thead><tr>';
|
|
20
|
+
html += '<th class="row-num">#</th>';
|
|
21
|
+
columns.forEach((col, idx) => {
|
|
22
|
+
const isSelected = datatableSelectedColumns.has(idx);
|
|
23
|
+
html += `<th class="${isSelected ? 'selected' : ''}" data-col="${idx}">
|
|
24
|
+
<div class="datatable-col-header">
|
|
25
|
+
<input type="checkbox"
|
|
26
|
+
data-col-idx="${idx}"
|
|
27
|
+
${isSelected ? 'checked' : ''}
|
|
28
|
+
onchange="toggleColumnSelection(${idx})">
|
|
29
|
+
<span class="col-name" title="${col.name}">${col.name}</span>
|
|
30
|
+
<span class="col-type">${col.type === 'numeric' ? 'N' : 'S'}</span>
|
|
31
|
+
</div>
|
|
32
|
+
</th>`;
|
|
33
|
+
});
|
|
34
|
+
html += '</tr></thead>';
|
|
35
|
+
|
|
36
|
+
// Data rows (limit to first 100 for performance)
|
|
37
|
+
html += '<tbody>';
|
|
38
|
+
const maxRows = Math.min(rows.length, 100);
|
|
39
|
+
for (let i = 0; i < maxRows; i++) {
|
|
40
|
+
html += `<tr data-row-idx="${i}">`;
|
|
41
|
+
html += `<td class="row-num">${i + 1}</td>`;
|
|
42
|
+
columns.forEach((col, colIdx) => {
|
|
43
|
+
const value = rows[i][colIdx];
|
|
44
|
+
const displayValue = value === null || value === undefined ? '' : value;
|
|
45
|
+
// Wrap in span for smart truncation without interfering with editing
|
|
46
|
+
html += `<td data-row="${i}" data-col="${colIdx}" tabindex="0" title="${displayValue}">
|
|
47
|
+
<span class="cell-text">${displayValue}</span>
|
|
48
|
+
</td>`;
|
|
49
|
+
});
|
|
50
|
+
html += '</tr>';
|
|
51
|
+
}
|
|
52
|
+
html += '</tbody></table>';
|
|
53
|
+
|
|
54
|
+
content.innerHTML = html;
|
|
55
|
+
|
|
56
|
+
// Attach cell event listeners for selection/editing/clipboard
|
|
57
|
+
if (typeof attachCellEventListeners === 'function') {
|
|
58
|
+
attachCellEventListeners();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Update selection info
|
|
62
|
+
updateSelectionInfo();
|
|
63
|
+
|
|
64
|
+
// Hide dropzone, show content
|
|
65
|
+
const dropzone = document.getElementById('datatable-dropzone');
|
|
66
|
+
if (dropzone) dropzone.style.display = 'none';
|
|
67
|
+
const toolbar = document.querySelector('.datatable-toolbar');
|
|
68
|
+
if (toolbar) toolbar.style.display = 'flex';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ============================================================================
|
|
72
|
+
// Column Selection (for backward compatibility)
|
|
73
|
+
// ============================================================================
|
|
74
|
+
function toggleColumnSelection(colIdx) {
|
|
75
|
+
if (datatableSelectedColumns.has(colIdx)) {
|
|
76
|
+
datatableSelectedColumns.delete(colIdx);
|
|
77
|
+
} else {
|
|
78
|
+
datatableSelectedColumns.add(colIdx);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const isSelected = datatableSelectedColumns.has(colIdx);
|
|
82
|
+
|
|
83
|
+
// Update header styling
|
|
84
|
+
const th = document.querySelector(`th:has(input[data-col-idx="${colIdx}"])`);
|
|
85
|
+
if (th) {
|
|
86
|
+
th.classList.toggle('selected', isSelected);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Update entire column cells highlighting
|
|
90
|
+
document.querySelectorAll(`td[data-col="${colIdx}"]`).forEach(td => {
|
|
91
|
+
td.classList.toggle('col-selected', isSelected);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
updateSelectionInfo();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function updateSelectionInfo() {
|
|
98
|
+
const info = document.getElementById('datatable-selection-info');
|
|
99
|
+
|
|
100
|
+
if (info) {
|
|
101
|
+
// Count assigned variables
|
|
102
|
+
const assignedCount = Object.keys(datatableVarAssignments).length;
|
|
103
|
+
if (assignedCount > 0) {
|
|
104
|
+
info.innerHTML = `<span class="selected-count">${assignedCount}</span> variable${assignedCount !== 1 ? 's' : ''} assigned`;
|
|
105
|
+
} else {
|
|
106
|
+
info.innerHTML = `<span class="selected-count">0</span> variables assigned`;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ============================================================================
|
|
112
|
+
// Canvas-Datatable Linking: Highlight columns for selected canvas element
|
|
113
|
+
// ============================================================================
|
|
114
|
+
let datatableLinkedCallId = null;
|
|
115
|
+
|
|
116
|
+
function highlightDatatableForElement(callId) {
|
|
117
|
+
datatableLinkedCallId = callId;
|
|
118
|
+
|
|
119
|
+
// Clear previous highlights (headers and data cells)
|
|
120
|
+
document.querySelectorAll('.datatable-table .canvas-linked').forEach(el => {
|
|
121
|
+
el.classList.remove('canvas-linked');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
if (!callId || !datatableData) return;
|
|
125
|
+
|
|
126
|
+
const columns = datatableData.columns || [];
|
|
127
|
+
const matchedIndices = [];
|
|
128
|
+
|
|
129
|
+
// Strategy 1: Match columns by prefix pattern (e.g., scatter_x, scatter_y)
|
|
130
|
+
columns.forEach((col, idx) => {
|
|
131
|
+
if (col.name.startsWith(callId + '_') || col.name === callId) {
|
|
132
|
+
matchedIndices.push(idx);
|
|
133
|
+
// Highlight header
|
|
134
|
+
const th = document.querySelector(`.datatable-table th:nth-child(${idx + 2})`);
|
|
135
|
+
if (th) th.classList.add('canvas-linked');
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Highlight data cells in matched columns
|
|
140
|
+
matchedIndices.forEach(idx => {
|
|
141
|
+
document.querySelectorAll(`.datatable-table tr td:nth-child(${idx + 2})`).forEach(td => {
|
|
142
|
+
td.classList.add('canvas-linked');
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
let hasMatch = matchedIndices.length > 0;
|
|
147
|
+
|
|
148
|
+
// Strategy 2: If in a matching tab, highlight ALL columns in that tab
|
|
149
|
+
// (handles shared x-data case for line plots where x column isn't prefixed)
|
|
150
|
+
if (typeof datatableTabs !== 'undefined' && typeof activeTabId !== 'undefined') {
|
|
151
|
+
const activeTab = datatableTabs[activeTabId];
|
|
152
|
+
if (activeTab && (activeTab.callId === callId || activeTab.name === callId)) {
|
|
153
|
+
columns.forEach((col, idx) => {
|
|
154
|
+
// Highlight header
|
|
155
|
+
const th = document.querySelector(`.datatable-table th:nth-child(${idx + 2})`);
|
|
156
|
+
if (th) {
|
|
157
|
+
th.classList.add('canvas-linked');
|
|
158
|
+
hasMatch = true;
|
|
159
|
+
}
|
|
160
|
+
// Highlight data cells
|
|
161
|
+
document.querySelectorAll(`.datatable-table tr td:nth-child(${idx + 2})`).forEach(td => {
|
|
162
|
+
td.classList.add('canvas-linked');
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Expand datatable panel if collapsed and has linked columns
|
|
169
|
+
const panel = document.getElementById('datatable-panel');
|
|
170
|
+
if (hasMatch && panel && !panel.classList.contains('expanded')) {
|
|
171
|
+
panel.classList.add('expanded');
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function clearDatatableHighlight() {
|
|
176
|
+
datatableLinkedCallId = null;
|
|
177
|
+
document.querySelectorAll('.datatable-table .canvas-linked').forEach(el => {
|
|
178
|
+
el.classList.remove('canvas-linked');
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Full sync: highlight columns + auto-select plot type + set target panel
|
|
183
|
+
function syncDatatableToElement(element) {
|
|
184
|
+
if (!element) {
|
|
185
|
+
clearDatatableHighlight();
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Skip highlighting for panel/axes selections - they don't have data columns
|
|
190
|
+
const elemType = element.type || '';
|
|
191
|
+
if (elemType === 'axes' || elemType === 'panel' || (element.label && element.label.startsWith('Panel '))) {
|
|
192
|
+
console.log('[Highlight] Skipping panel/axes element');
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const callId = element.call_id || element.label;
|
|
197
|
+
|
|
198
|
+
// 1. Highlight matching columns
|
|
199
|
+
highlightDatatableForElement(callId);
|
|
200
|
+
|
|
201
|
+
// 2. Auto-select plot type if element has a function type
|
|
202
|
+
if (element.function) {
|
|
203
|
+
const plotSelect = document.getElementById('datatable-plot-type');
|
|
204
|
+
if (plotSelect) {
|
|
205
|
+
// Find matching option
|
|
206
|
+
const options = plotSelect.querySelectorAll('option');
|
|
207
|
+
for (const opt of options) {
|
|
208
|
+
if (opt.value === element.function) {
|
|
209
|
+
plotSelect.value = element.function;
|
|
210
|
+
datatablePlotType = element.function;
|
|
211
|
+
if (typeof updateVarAssignSlots === 'function') updateVarAssignSlots();
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// 3. Set target panel to the element's axis
|
|
219
|
+
if (element.ax_index !== undefined) {
|
|
220
|
+
const targetSelect = document.getElementById('datatable-target-panel');
|
|
221
|
+
if (targetSelect) {
|
|
222
|
+
targetSelect.value = element.ax_index;
|
|
223
|
+
datatableTargetAxis = element.ax_index;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// 4. Switch to matching datatable tab
|
|
228
|
+
if (typeof datatableTabs !== 'undefined') {
|
|
229
|
+
// Try multiple matching strategies
|
|
230
|
+
const elemKey = element.key || '';
|
|
231
|
+
const elemType = element.type || '';
|
|
232
|
+
|
|
233
|
+
for (const [tabId, tabState] of Object.entries(datatableTabs)) {
|
|
234
|
+
const tabName = (tabState.callId || tabState.name || '').toLowerCase();
|
|
235
|
+
// Match: exact callId, name match, or element key contains tab name
|
|
236
|
+
const matches = (callId && (tabState.callId === callId || tabState.name === callId)) ||
|
|
237
|
+
(elemKey && elemKey.toLowerCase().includes(tabName)) ||
|
|
238
|
+
(elemType && tabName.includes(elemType.toLowerCase()));
|
|
239
|
+
if (matches) {
|
|
240
|
+
if (activeTabId !== tabId && typeof selectTab === 'function') {
|
|
241
|
+
// Use internal flag to prevent sync loop
|
|
242
|
+
window._syncingFromCanvasToData = true;
|
|
243
|
+
selectTab(tabId);
|
|
244
|
+
window._syncingFromCanvasToData = false;
|
|
245
|
+
console.log('[Datatable] Canvas->Data: Switched to tab', tabName);
|
|
246
|
+
}
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
"""
|
|
253
|
+
|
|
254
|
+
__all__ = ["JS_DATATABLE_TABLE"]
|
|
255
|
+
|
|
256
|
+
# EOF
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Datatable tab management JavaScript."""
|
|
4
|
+
|
|
5
|
+
JS_DATATABLE_TABS = """
|
|
6
|
+
// ============================================================================
|
|
7
|
+
// Multi-Tab Datatable State
|
|
8
|
+
// ============================================================================
|
|
9
|
+
let datatableTabs = {}; // Maps tabId -> {data, selectedColumns, varAssignments, plotType, targetAxis}
|
|
10
|
+
let activeTabId = null;
|
|
11
|
+
let tabCounter = 0;
|
|
12
|
+
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// Tab Management
|
|
15
|
+
// ============================================================================
|
|
16
|
+
function initDatatableTabs() {
|
|
17
|
+
console.log('[Datatable] initDatatableTabs called');
|
|
18
|
+
const newTabBtn = document.getElementById('btn-new-tab');
|
|
19
|
+
if (newTabBtn) {
|
|
20
|
+
newTabBtn.addEventListener('click', createNewTab);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Load tabs from figure data
|
|
24
|
+
loadTabsFromFigure();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function loadTabsFromFigure() {
|
|
28
|
+
// Fetch existing plot data and calls to get axis info
|
|
29
|
+
Promise.all([
|
|
30
|
+
fetch('/datatable/data').then(r => r.json()),
|
|
31
|
+
fetch('/calls').then(r => r.json())
|
|
32
|
+
]).then(([data, calls]) => {
|
|
33
|
+
console.log('[Datatable] Loaded data:', data.columns?.length || 0, 'columns');
|
|
34
|
+
if (data.columns && data.columns.length > 0) {
|
|
35
|
+
// Build a map of call_id -> axis index from calls
|
|
36
|
+
// calls is an object: {call_id: {function, ax_key, ...}, ...}
|
|
37
|
+
const callToAxis = {};
|
|
38
|
+
const callToFunction = {};
|
|
39
|
+
Object.entries(calls).forEach(([callId, call]) => {
|
|
40
|
+
// Extract axis index from ax_key (e.g., "ax_0_1" -> row=0, col=1 -> panel index)
|
|
41
|
+
let axIndex = 0;
|
|
42
|
+
if (call.ax_key) {
|
|
43
|
+
const match = call.ax_key.match(/ax_(\\d+)_(\\d+)/);
|
|
44
|
+
if (match) {
|
|
45
|
+
const row = parseInt(match[1]);
|
|
46
|
+
const col = parseInt(match[2]);
|
|
47
|
+
// For now, use simple linear indexing (works for 1xN grids)
|
|
48
|
+
// TODO: get actual ncols from axes_positions for proper MxN support
|
|
49
|
+
axIndex = col > 0 ? col : row;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
callToAxis[callId] = axIndex;
|
|
53
|
+
callToFunction[callId] = call.function || 'plot';
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Group columns by call_id (e.g., plot_000_x, plot_000_y -> plot_000)
|
|
57
|
+
// First pass: identify which columns belong to each group
|
|
58
|
+
const groups = {};
|
|
59
|
+
const colIndexMap = {}; // Maps callId -> [column indices]
|
|
60
|
+
data.columns.forEach((col, idx) => {
|
|
61
|
+
// Extract call_id from column name (e.g., "plot_000_x" -> "plot_000")
|
|
62
|
+
const match = col.name.match(/^(.+?)_[xy]$/);
|
|
63
|
+
const callId = match ? match[1] : col.name;
|
|
64
|
+
if (!groups[callId]) {
|
|
65
|
+
groups[callId] = {
|
|
66
|
+
columns: [],
|
|
67
|
+
axIndex: callToAxis[callId] !== undefined ? callToAxis[callId] : 0,
|
|
68
|
+
plotType: callToFunction[callId] || 'plot'
|
|
69
|
+
};
|
|
70
|
+
colIndexMap[callId] = [];
|
|
71
|
+
}
|
|
72
|
+
// Store column with new index for this group
|
|
73
|
+
groups[callId].columns.push({
|
|
74
|
+
...col,
|
|
75
|
+
index: groups[callId].columns.length
|
|
76
|
+
});
|
|
77
|
+
colIndexMap[callId].push(idx);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Second pass: extract rows for each group (only relevant columns)
|
|
81
|
+
Object.keys(groups).forEach(callId => {
|
|
82
|
+
const indices = colIndexMap[callId];
|
|
83
|
+
groups[callId].rows = data.rows.map(row =>
|
|
84
|
+
indices.map(idx => row[idx])
|
|
85
|
+
);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Create a tab for each group with element colors
|
|
89
|
+
console.log('[Datatable] Creating tabs for', Object.keys(groups).length, 'groups:', Object.keys(groups));
|
|
90
|
+
Object.entries(groups).forEach(([callId, groupData]) => {
|
|
91
|
+
// Get element color from colorMap (populated by hitmap)
|
|
92
|
+
const elemColor = typeof getGroupRepresentativeColor === 'function'
|
|
93
|
+
? getGroupRepresentativeColor(callId, null)
|
|
94
|
+
: null;
|
|
95
|
+
console.log('[Datatable] Creating tab:', callId, 'with', groupData.columns?.length, 'columns,', groupData.rows?.length, 'rows');
|
|
96
|
+
createTab(callId, groupData, false, groupData.axIndex, groupData.plotType, elemColor);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Select first tab
|
|
100
|
+
const firstTabId = Object.keys(datatableTabs)[0];
|
|
101
|
+
if (firstTabId) {
|
|
102
|
+
selectTab(firstTabId);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Retry color update after hitmap loads (may not be ready yet)
|
|
106
|
+
setTimeout(() => { if (typeof updateTabColors === 'function') updateTabColors(); }, 500);
|
|
107
|
+
}
|
|
108
|
+
}).catch(err => {
|
|
109
|
+
console.error('Failed to load tabs from figure:', err);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function createTab(name, data = null, select = true, axIndex = null, plotType = 'plot', elementColor = null) {
|
|
114
|
+
const tabId = `tab_${tabCounter++}`;
|
|
115
|
+
const tabList = document.getElementById('datatable-tab-list');
|
|
116
|
+
|
|
117
|
+
// Create tab element with axis badge
|
|
118
|
+
const tab = document.createElement('button');
|
|
119
|
+
tab.className = 'datatable-tab';
|
|
120
|
+
tab.dataset.tabId = tabId;
|
|
121
|
+
// Use letters A, B, C... to match subplot labels in figure
|
|
122
|
+
const axLabel = axIndex !== null ? String.fromCharCode(65 + axIndex) : null; // 65 is 'A'
|
|
123
|
+
const axBadge = axLabel ? `<span class="tab-axis" title="Panel ${axLabel}">${axLabel}</span>` : '';
|
|
124
|
+
tab.innerHTML = `
|
|
125
|
+
${axBadge}
|
|
126
|
+
<span class="tab-name" title="${name}">${name}</span>
|
|
127
|
+
<span class="tab-close" onclick="event.stopPropagation(); closeTab('${tabId}')">×</span>
|
|
128
|
+
`;
|
|
129
|
+
tab.onclick = () => selectTab(tabId);
|
|
130
|
+
|
|
131
|
+
// Apply element color if available
|
|
132
|
+
if (elementColor) {
|
|
133
|
+
tab.style.setProperty('--element-color', elementColor);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
tabList.appendChild(tab);
|
|
137
|
+
|
|
138
|
+
// Initialize tab state with axis association
|
|
139
|
+
datatableTabs[tabId] = {
|
|
140
|
+
name: name,
|
|
141
|
+
data: data,
|
|
142
|
+
selectedColumns: new Set(),
|
|
143
|
+
varAssignments: {},
|
|
144
|
+
varColors: {},
|
|
145
|
+
plotType: plotType,
|
|
146
|
+
targetAxis: axIndex, // Associated axis
|
|
147
|
+
callId: name, // Original call_id for linking
|
|
148
|
+
elementColor: elementColor // Element's original color
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
if (select) {
|
|
152
|
+
selectTab(tabId);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return tabId;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function createNewTab() {
|
|
159
|
+
const name = `Data ${tabCounter + 1}`;
|
|
160
|
+
// Auto-create large empty editable table (matches createNewCSV defaults)
|
|
161
|
+
const numRows = 100;
|
|
162
|
+
const numCols = 5;
|
|
163
|
+
|
|
164
|
+
const columns = [];
|
|
165
|
+
for (let i = 0; i < numCols; i++) {
|
|
166
|
+
// Default to 'string' type so users can type anything
|
|
167
|
+
columns.push({ name: `col${i + 1}`, type: 'string', index: i });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const rows = [];
|
|
171
|
+
for (let i = 0; i < numRows; i++) {
|
|
172
|
+
rows.push(new Array(numCols).fill(''));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const defaultData = { columns, rows };
|
|
176
|
+
createTab(name, defaultData, true);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function selectTab(tabId) {
|
|
180
|
+
if (!datatableTabs[tabId]) return;
|
|
181
|
+
|
|
182
|
+
// Save current tab state
|
|
183
|
+
saveCurrentTabState();
|
|
184
|
+
|
|
185
|
+
// Update active tab
|
|
186
|
+
activeTabId = tabId;
|
|
187
|
+
|
|
188
|
+
// Update tab UI
|
|
189
|
+
document.querySelectorAll('.datatable-tab').forEach(tab => {
|
|
190
|
+
tab.classList.toggle('active', tab.dataset.tabId === tabId);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// Restore tab state
|
|
194
|
+
restoreTabState(tabId);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function closeTab(tabId) {
|
|
198
|
+
const tab = document.querySelector(`.datatable-tab[data-tab-id="${tabId}"]`);
|
|
199
|
+
if (tab) {
|
|
200
|
+
tab.remove();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
delete datatableTabs[tabId];
|
|
204
|
+
|
|
205
|
+
// If closing active tab, select another
|
|
206
|
+
if (activeTabId === tabId) {
|
|
207
|
+
const remainingTabs = Object.keys(datatableTabs);
|
|
208
|
+
if (remainingTabs.length > 0) {
|
|
209
|
+
selectTab(remainingTabs[0]);
|
|
210
|
+
} else {
|
|
211
|
+
activeTabId = null;
|
|
212
|
+
clearDatatableDisplay();
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function saveCurrentTabState() {
|
|
218
|
+
if (!activeTabId || !datatableTabs[activeTabId]) return;
|
|
219
|
+
|
|
220
|
+
datatableTabs[activeTabId].data = datatableData;
|
|
221
|
+
datatableTabs[activeTabId].selectedColumns = new Set(datatableSelectedColumns);
|
|
222
|
+
datatableTabs[activeTabId].varAssignments = {...datatableVarAssignments};
|
|
223
|
+
datatableTabs[activeTabId].varColors = {...datatableVarColors};
|
|
224
|
+
datatableTabs[activeTabId].plotType = datatablePlotType;
|
|
225
|
+
datatableTabs[activeTabId].targetAxis = datatableTargetAxis;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function restoreTabState(tabId) {
|
|
229
|
+
const state = datatableTabs[tabId];
|
|
230
|
+
if (!state) return;
|
|
231
|
+
|
|
232
|
+
// Restore global state
|
|
233
|
+
datatableData = state.data;
|
|
234
|
+
datatableSelectedColumns = new Set(state.selectedColumns);
|
|
235
|
+
datatableVarAssignments = {...state.varAssignments};
|
|
236
|
+
datatableVarColors = state.varColors ? {...state.varColors} : {};
|
|
237
|
+
datatablePlotType = state.plotType || 'plot';
|
|
238
|
+
datatableTargetAxis = state.targetAxis;
|
|
239
|
+
|
|
240
|
+
// Update UI
|
|
241
|
+
if (datatableData) {
|
|
242
|
+
// Use renderEditableTable for direct editing capability
|
|
243
|
+
if (typeof renderEditableTable === 'function') {
|
|
244
|
+
renderEditableTable();
|
|
245
|
+
} else {
|
|
246
|
+
renderDatatable();
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Show toolbar, hide entire import section
|
|
250
|
+
const importSection = document.getElementById('datatable-import-section');
|
|
251
|
+
if (importSection) importSection.style.display = 'none';
|
|
252
|
+
const toolbar = document.querySelector('.datatable-toolbar');
|
|
253
|
+
if (toolbar) toolbar.style.display = 'flex';
|
|
254
|
+
|
|
255
|
+
// Restore plot type selection
|
|
256
|
+
const plotTypeSelect = document.getElementById('datatable-plot-type');
|
|
257
|
+
if (plotTypeSelect) plotTypeSelect.value = datatablePlotType;
|
|
258
|
+
|
|
259
|
+
// Restore target panel selection
|
|
260
|
+
const targetPanelSelect = document.getElementById('datatable-target-panel');
|
|
261
|
+
if (targetPanelSelect && datatableTargetAxis !== null) {
|
|
262
|
+
targetPanelSelect.value = datatableTargetAxis;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Update variable assignment UI
|
|
266
|
+
updateVarAssignSlots();
|
|
267
|
+
} else {
|
|
268
|
+
clearDatatableDisplay();
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function clearDatatableDisplay() {
|
|
273
|
+
const content = document.getElementById('datatable-content');
|
|
274
|
+
if (content) content.innerHTML = '';
|
|
275
|
+
|
|
276
|
+
// Show import section with dropzone
|
|
277
|
+
const importSection = document.getElementById('datatable-import-section');
|
|
278
|
+
if (importSection) importSection.style.display = 'block';
|
|
279
|
+
|
|
280
|
+
const toolbar = document.querySelector('.datatable-toolbar');
|
|
281
|
+
if (toolbar) toolbar.style.display = 'none';
|
|
282
|
+
|
|
283
|
+
const varAssign = document.getElementById('datatable-var-assign');
|
|
284
|
+
if (varAssign) varAssign.style.display = 'none';
|
|
285
|
+
|
|
286
|
+
updateSelectionInfo();
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Override loadExistingData to use tabs
|
|
290
|
+
function loadExistingData() {
|
|
291
|
+
// This is now handled by loadTabsFromFigure
|
|
292
|
+
// But we still call initDatatableTabs
|
|
293
|
+
initDatatableTabs();
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Override handleParsedData to work with tabs
|
|
297
|
+
const originalHandleParsedData = typeof handleParsedData === 'function' ? handleParsedData : null;
|
|
298
|
+
|
|
299
|
+
function handleParsedDataWithTabs(parsedData) {
|
|
300
|
+
if (!activeTabId) {
|
|
301
|
+
// Create a new tab for imported data
|
|
302
|
+
createNewTab();
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Store in current tab
|
|
306
|
+
datatableTabs[activeTabId].data = parsedData;
|
|
307
|
+
datatableData = parsedData;
|
|
308
|
+
|
|
309
|
+
renderDatatable();
|
|
310
|
+
|
|
311
|
+
// Show toolbar, hide entire import section
|
|
312
|
+
const importSection = document.getElementById('datatable-import-section');
|
|
313
|
+
if (importSection) importSection.style.display = 'none';
|
|
314
|
+
const toolbar = document.querySelector('.datatable-toolbar');
|
|
315
|
+
if (toolbar) toolbar.style.display = 'flex';
|
|
316
|
+
|
|
317
|
+
updateVarAssignSlots();
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Hook into handleParsedData
|
|
321
|
+
if (typeof window !== 'undefined') {
|
|
322
|
+
window.handleParsedData = handleParsedDataWithTabs;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Update tab colors after colorMap is loaded
|
|
326
|
+
function updateTabColors() {
|
|
327
|
+
if (typeof colorMap === 'undefined' || !colorMap) return;
|
|
328
|
+
if (typeof getGroupRepresentativeColor !== 'function') return;
|
|
329
|
+
|
|
330
|
+
Object.entries(datatableTabs).forEach(([tabId, tabState]) => {
|
|
331
|
+
if (tabState.callId) {
|
|
332
|
+
// Always get the latest color from colorMap (override any stale color)
|
|
333
|
+
const elemColor = getGroupRepresentativeColor(tabState.callId, null);
|
|
334
|
+
if (elemColor) {
|
|
335
|
+
tabState.elementColor = elemColor;
|
|
336
|
+
const tabEl = document.querySelector(`.datatable-tab[data-tab-id="${tabId}"]`);
|
|
337
|
+
if (tabEl) {
|
|
338
|
+
tabEl.style.setProperty('--element-color', elemColor);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Export for hitmap to call after colorMap is loaded
|
|
346
|
+
if (typeof window !== 'undefined') {
|
|
347
|
+
window.updateTabColors = updateTabColors;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
"""
|
|
351
|
+
|
|
352
|
+
__all__ = ["JS_DATATABLE_TABS"]
|
|
353
|
+
|
|
354
|
+
# EOF
|
|
@@ -94,7 +94,18 @@ function showDynamicCallProperties(element) {
|
|
|
94
94
|
|
|
95
95
|
container.innerHTML = '';
|
|
96
96
|
|
|
97
|
-
|
|
97
|
+
// Get call_id - try element directly, then colorMap, then label
|
|
98
|
+
let callId = element.call_id;
|
|
99
|
+
if (!callId && element.key && typeof colorMap !== 'undefined') {
|
|
100
|
+
// Look up call_id from colorMap (hitmap has this info)
|
|
101
|
+
const colorInfo = colorMap[element.key];
|
|
102
|
+
if (colorInfo && colorInfo.call_id) {
|
|
103
|
+
callId = colorInfo.call_id;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (!callId) {
|
|
107
|
+
callId = element.label;
|
|
108
|
+
}
|
|
98
109
|
|
|
99
110
|
// If no call data found, show basic element info instead
|
|
100
111
|
if (!callId || !callsData[callId]) {
|
|
@@ -286,7 +297,11 @@ async function handleDynamicParamChange(callId, param, input) {
|
|
|
286
297
|
loadHitmap();
|
|
287
298
|
updateHitRegions();
|
|
288
299
|
|
|
289
|
-
|
|
300
|
+
// Sync callsData from server response (source of truth)
|
|
301
|
+
if (callsData[callId] && data.updated_call) {
|
|
302
|
+
callsData[callId].kwargs = data.updated_call.kwargs;
|
|
303
|
+
} else if (callsData[callId]) {
|
|
304
|
+
// Fallback: manual update
|
|
290
305
|
if (value === null) {
|
|
291
306
|
delete callsData[callId].kwargs[param];
|
|
292
307
|
} else {
|