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,511 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Datatable editable table JavaScript - create and edit tables manually."""
|
|
4
|
+
|
|
5
|
+
JS_DATATABLE_EDITABLE = """
|
|
6
|
+
// ============================================================================
|
|
7
|
+
// Create New CSV (Empty Editable Table)
|
|
8
|
+
// ============================================================================
|
|
9
|
+
const DEFAULT_ROWS = 100; // Large table by default (vis_app uses 1000)
|
|
10
|
+
const DEFAULT_COLS = 5;
|
|
11
|
+
const ROWS_PER_BATCH = 100; // Rows to load per scroll batch
|
|
12
|
+
const COLS_PER_BATCH = 20; // Columns to load per scroll batch
|
|
13
|
+
let datatableRenderedRows = 0; // Track how many rows are currently rendered
|
|
14
|
+
let datatableRenderedCols = 0; // Track how many columns are currently rendered
|
|
15
|
+
let datatableIsLoadingMore = false; // Prevent multiple concurrent loads
|
|
16
|
+
|
|
17
|
+
function createNewCSV() {
|
|
18
|
+
// Create default structure with more columns and rows for real data entry
|
|
19
|
+
const columns = [];
|
|
20
|
+
for (let i = 0; i < DEFAULT_COLS; i++) {
|
|
21
|
+
// Default to 'string' type so users can type anything (change to N if needed)
|
|
22
|
+
columns.push({ name: `col${i + 1}`, type: 'string', index: i });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const rows = [];
|
|
26
|
+
for (let i = 0; i < DEFAULT_ROWS; i++) {
|
|
27
|
+
rows.push(new Array(DEFAULT_COLS).fill(''));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const defaultData = { columns, rows };
|
|
31
|
+
|
|
32
|
+
// Set as current data
|
|
33
|
+
datatableData = defaultData;
|
|
34
|
+
datatableSelectedColumns = new Set();
|
|
35
|
+
datatableVarAssignments = {};
|
|
36
|
+
|
|
37
|
+
// Render editable table
|
|
38
|
+
renderEditableTable();
|
|
39
|
+
|
|
40
|
+
// Show toolbar, hide entire import section
|
|
41
|
+
const importSection = document.getElementById('datatable-import-section');
|
|
42
|
+
if (importSection) importSection.style.display = 'none';
|
|
43
|
+
|
|
44
|
+
const toolbar = document.querySelector('.datatable-toolbar');
|
|
45
|
+
if (toolbar) toolbar.style.display = 'flex';
|
|
46
|
+
|
|
47
|
+
updateVarAssignSlots();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ============================================================================
|
|
51
|
+
// Render Editable Table (vis_app pattern: span cells, no inline inputs)
|
|
52
|
+
// Uses event delegation for cell selection, editing, and clipboard
|
|
53
|
+
// ============================================================================
|
|
54
|
+
function renderEditableTable() {
|
|
55
|
+
const content = document.getElementById('datatable-content');
|
|
56
|
+
if (!content || !datatableData) return;
|
|
57
|
+
|
|
58
|
+
const { columns, rows } = datatableData;
|
|
59
|
+
|
|
60
|
+
// Initial render: show first batch of columns
|
|
61
|
+
const initialCols = Math.min(columns.length, COLS_PER_BATCH);
|
|
62
|
+
datatableRenderedCols = initialCols;
|
|
63
|
+
|
|
64
|
+
let html = '<div class="editable-table-wrapper">';
|
|
65
|
+
|
|
66
|
+
// Build editable table with scroll container
|
|
67
|
+
html += '<div class="editable-table-scroll">';
|
|
68
|
+
html += '<table class="datatable-table editable" tabindex="0">';
|
|
69
|
+
|
|
70
|
+
// Header row with editable column names and selection checkboxes
|
|
71
|
+
html += '<thead><tr id="datatable-thead-row">';
|
|
72
|
+
html += '<th class="row-num">#</th>';
|
|
73
|
+
const elemColor = getCurrentTabElementColor();
|
|
74
|
+
for (let idx = 0; idx < initialCols; idx++) {
|
|
75
|
+
html += renderColumnHeader(idx, columns[idx], elemColor);
|
|
76
|
+
}
|
|
77
|
+
html += '</tr></thead>';
|
|
78
|
+
|
|
79
|
+
// Data rows with span-wrapped cells (vis_app pattern)
|
|
80
|
+
// Uses event delegation - no per-cell handlers
|
|
81
|
+
html += '<tbody id="datatable-tbody">';
|
|
82
|
+
// Initial render: show first batch of rows and columns
|
|
83
|
+
const initialRows = Math.min(rows.length, ROWS_PER_BATCH);
|
|
84
|
+
datatableRenderedRows = initialRows;
|
|
85
|
+
const colsToRender = columns.slice(0, initialCols);
|
|
86
|
+
for (let rowIdx = 0; rowIdx < initialRows; rowIdx++) {
|
|
87
|
+
html += renderTableRow(rowIdx, colsToRender, rows[rowIdx]);
|
|
88
|
+
}
|
|
89
|
+
html += '</tbody></table>';
|
|
90
|
+
html += '</div>'; // Close scroll container
|
|
91
|
+
|
|
92
|
+
html += '</div>'; // Close wrapper
|
|
93
|
+
|
|
94
|
+
content.innerHTML = html;
|
|
95
|
+
|
|
96
|
+
// Attach event delegation listeners for selection/editing/clipboard
|
|
97
|
+
if (typeof attachCellEventListeners === 'function') {
|
|
98
|
+
attachCellEventListeners();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Attach scroll listener for infinite scroll (both vertical and horizontal)
|
|
102
|
+
attachScrollListener();
|
|
103
|
+
|
|
104
|
+
updateSelectionInfo();
|
|
105
|
+
|
|
106
|
+
// Restore cell selection display if any
|
|
107
|
+
if (typeof updateCellSelectionDisplay === 'function') {
|
|
108
|
+
updateCellSelectionDisplay();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Helper function to render a column header (with optional element color)
|
|
113
|
+
function renderColumnHeader(idx, col, elementColor = null) {
|
|
114
|
+
const isSelected = datatableSelectedColumns && datatableSelectedColumns.has(idx);
|
|
115
|
+
// Apply element color as left border if available
|
|
116
|
+
const colorStyle = elementColor ? `style="border-left: 3px solid ${elementColor};"` : '';
|
|
117
|
+
const colorClass = elementColor ? 'has-element-color' : '';
|
|
118
|
+
return `<th class="${isSelected ? 'selected' : ''} ${colorClass}" data-col="${idx}" ${colorStyle}>
|
|
119
|
+
<div class="datatable-col-header editable-header">
|
|
120
|
+
<input type="checkbox"
|
|
121
|
+
data-col-idx="${idx}"
|
|
122
|
+
${isSelected ? 'checked' : ''}
|
|
123
|
+
onchange="toggleColumnSelection(${idx}); renderEditableTable();">
|
|
124
|
+
<input type="text" class="col-name-input" value="${col.name}"
|
|
125
|
+
onchange="updateColumnName(${idx}, this.value)"
|
|
126
|
+
onclick="event.stopPropagation()">
|
|
127
|
+
<select class="col-type-select" onchange="updateColumnType(${idx}, this.value)"
|
|
128
|
+
title="${col.type === 'numeric' ? 'Numerical Column' : 'String Column'}">
|
|
129
|
+
<option value="numeric" ${col.type === 'numeric' ? 'selected' : ''} title="Numerical Column">Num</option>
|
|
130
|
+
<option value="string" ${col.type === 'string' ? 'selected' : ''} title="String Column">Str</option>
|
|
131
|
+
</select>
|
|
132
|
+
</div>
|
|
133
|
+
</th>`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Get element color for current tab
|
|
137
|
+
function getCurrentTabElementColor() {
|
|
138
|
+
if (!activeTabId || !datatableTabs[activeTabId]) return null;
|
|
139
|
+
return datatableTabs[activeTabId].elementColor || null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Helper function to render a single row (renders all provided columns)
|
|
143
|
+
function renderTableRow(rowIdx, columns, rowData) {
|
|
144
|
+
let html = `<tr data-row-idx="${rowIdx}">`;
|
|
145
|
+
html += `<td class="row-num">${rowIdx + 1}</td>`;
|
|
146
|
+
// Render all columns passed (caller controls which columns via slice)
|
|
147
|
+
for (let colIdx = 0; colIdx < columns.length; colIdx++) {
|
|
148
|
+
const col = columns[colIdx];
|
|
149
|
+
const rawValue = rowData[colIdx];
|
|
150
|
+
const value = rawValue !== null && rawValue !== undefined ? rawValue : '';
|
|
151
|
+
const displayValue = formatCellValue(value, col.type);
|
|
152
|
+
const isColSelected = datatableSelectedColumns && datatableSelectedColumns.has(colIdx);
|
|
153
|
+
// title shows full precision on hover
|
|
154
|
+
html += `<td data-row="${rowIdx}" data-col="${colIdx}" tabindex="0"
|
|
155
|
+
class="${isColSelected ? 'col-selected' : ''}" title="${value}" data-raw="${value}">
|
|
156
|
+
<span class="cell-text">${displayValue}</span>
|
|
157
|
+
</td>`;
|
|
158
|
+
}
|
|
159
|
+
html += '</tr>';
|
|
160
|
+
return html;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Smart cell value formatting (like Excel)
|
|
164
|
+
function formatCellValue(value, colType) {
|
|
165
|
+
if (value === null || value === undefined || value === '') return '';
|
|
166
|
+
|
|
167
|
+
// For numeric columns, apply smart precision
|
|
168
|
+
if (colType === 'numeric' || typeof value === 'number') {
|
|
169
|
+
const num = parseFloat(value);
|
|
170
|
+
if (isNaN(num)) return String(value);
|
|
171
|
+
|
|
172
|
+
// Integer check - show without decimals
|
|
173
|
+
if (Number.isInteger(num)) return num.toString();
|
|
174
|
+
|
|
175
|
+
// Scientific notation for very large/small numbers
|
|
176
|
+
if (Math.abs(num) >= 1e10 || (Math.abs(num) < 1e-4 && num !== 0)) {
|
|
177
|
+
return num.toExponential(3);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Smart decimal places: show up to 4 significant decimal places
|
|
181
|
+
// but remove trailing zeros
|
|
182
|
+
const fixed = num.toFixed(6);
|
|
183
|
+
// Remove trailing zeros after decimal point
|
|
184
|
+
return parseFloat(fixed).toString();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// String values - truncate if too long
|
|
188
|
+
const str = String(value);
|
|
189
|
+
if (str.length > 30) {
|
|
190
|
+
return str.substring(0, 27) + '...';
|
|
191
|
+
}
|
|
192
|
+
return str;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Attach scroll listener for infinite scroll / lazy loading
|
|
196
|
+
function attachScrollListener() {
|
|
197
|
+
const scrollContainer = document.querySelector('.editable-table-scroll');
|
|
198
|
+
if (!scrollContainer) return;
|
|
199
|
+
|
|
200
|
+
scrollContainer.addEventListener('scroll', handleTableScroll);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Handle scroll event - load more rows/columns when near edges
|
|
204
|
+
function handleTableScroll(e) {
|
|
205
|
+
if (datatableIsLoadingMore || !datatableData) return;
|
|
206
|
+
|
|
207
|
+
const container = e.target;
|
|
208
|
+
const { scrollTop, scrollHeight, clientHeight, scrollLeft, scrollWidth, clientWidth } = container;
|
|
209
|
+
|
|
210
|
+
// Load more rows when within 100px of bottom
|
|
211
|
+
if (scrollHeight - scrollTop - clientHeight < 100) {
|
|
212
|
+
loadMoreRows();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Load more columns when within 100px of right edge
|
|
216
|
+
if (scrollWidth - scrollLeft - clientWidth < 100) {
|
|
217
|
+
loadMoreColumns();
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Check if the current table is an editable/new table (not plotted data)
|
|
222
|
+
function isEditableTable() {
|
|
223
|
+
const table = document.querySelector('.datatable-table.editable');
|
|
224
|
+
return table !== null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Load next batch of rows (or auto-expand for editable tables)
|
|
228
|
+
function loadMoreRows() {
|
|
229
|
+
if (!datatableData || datatableIsLoadingMore) return;
|
|
230
|
+
|
|
231
|
+
const { columns, rows } = datatableData;
|
|
232
|
+
|
|
233
|
+
// For editable tables: auto-expand by adding new empty rows when at bottom
|
|
234
|
+
if (datatableRenderedRows >= rows.length && isEditableTable()) {
|
|
235
|
+
autoExpandRows();
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (datatableRenderedRows >= rows.length) return; // All rows loaded (non-editable)
|
|
240
|
+
|
|
241
|
+
datatableIsLoadingMore = true;
|
|
242
|
+
|
|
243
|
+
const tbody = document.getElementById('datatable-tbody');
|
|
244
|
+
if (!tbody) {
|
|
245
|
+
datatableIsLoadingMore = false;
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Calculate next batch
|
|
250
|
+
const startIdx = datatableRenderedRows;
|
|
251
|
+
const endIdx = Math.min(startIdx + ROWS_PER_BATCH, rows.length);
|
|
252
|
+
|
|
253
|
+
// Append new rows (only render columns up to datatableRenderedCols)
|
|
254
|
+
let newRowsHtml = '';
|
|
255
|
+
const colsToRender = columns.slice(0, datatableRenderedCols);
|
|
256
|
+
for (let rowIdx = startIdx; rowIdx < endIdx; rowIdx++) {
|
|
257
|
+
newRowsHtml += renderTableRow(rowIdx, colsToRender, rows[rowIdx]);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Use insertAdjacentHTML for performance (doesn't re-parse existing DOM)
|
|
261
|
+
tbody.insertAdjacentHTML('beforeend', newRowsHtml);
|
|
262
|
+
|
|
263
|
+
datatableRenderedRows = endIdx;
|
|
264
|
+
datatableIsLoadingMore = false;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Auto-expand: add new empty rows to editable table when scrolling to bottom
|
|
268
|
+
function autoExpandRows() {
|
|
269
|
+
if (!datatableData || datatableIsLoadingMore) return;
|
|
270
|
+
|
|
271
|
+
datatableIsLoadingMore = true;
|
|
272
|
+
|
|
273
|
+
const { columns, rows } = datatableData;
|
|
274
|
+
const tbody = document.getElementById('datatable-tbody');
|
|
275
|
+
if (!tbody) {
|
|
276
|
+
datatableIsLoadingMore = false;
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Add ROWS_PER_BATCH new empty rows to data
|
|
281
|
+
const startIdx = rows.length;
|
|
282
|
+
for (let i = 0; i < ROWS_PER_BATCH; i++) {
|
|
283
|
+
rows.push(new Array(columns.length).fill(''));
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Render the new rows
|
|
287
|
+
let newRowsHtml = '';
|
|
288
|
+
const colsToRender = columns.slice(0, datatableRenderedCols);
|
|
289
|
+
for (let rowIdx = startIdx; rowIdx < rows.length; rowIdx++) {
|
|
290
|
+
newRowsHtml += renderTableRow(rowIdx, colsToRender, rows[rowIdx]);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
tbody.insertAdjacentHTML('beforeend', newRowsHtml);
|
|
294
|
+
datatableRenderedRows = rows.length;
|
|
295
|
+
datatableIsLoadingMore = false;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Load next batch of columns (or auto-expand for editable tables)
|
|
299
|
+
function loadMoreColumns() {
|
|
300
|
+
if (!datatableData || datatableIsLoadingMore) return;
|
|
301
|
+
|
|
302
|
+
const { columns, rows } = datatableData;
|
|
303
|
+
|
|
304
|
+
// For editable tables: auto-expand by adding new columns when at right edge
|
|
305
|
+
if (datatableRenderedCols >= columns.length && isEditableTable()) {
|
|
306
|
+
autoExpandColumns();
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (datatableRenderedCols >= columns.length) return; // All columns loaded (non-editable)
|
|
311
|
+
|
|
312
|
+
datatableIsLoadingMore = true;
|
|
313
|
+
|
|
314
|
+
const theadRow = document.getElementById('datatable-thead-row');
|
|
315
|
+
const tbody = document.getElementById('datatable-tbody');
|
|
316
|
+
if (!theadRow || !tbody) {
|
|
317
|
+
datatableIsLoadingMore = false;
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Calculate next batch of columns
|
|
322
|
+
const startCol = datatableRenderedCols;
|
|
323
|
+
const endCol = Math.min(startCol + COLS_PER_BATCH, columns.length);
|
|
324
|
+
|
|
325
|
+
// Add new column headers
|
|
326
|
+
let newHeadersHtml = '';
|
|
327
|
+
const elemColor = getCurrentTabElementColor();
|
|
328
|
+
for (let colIdx = startCol; colIdx < endCol; colIdx++) {
|
|
329
|
+
newHeadersHtml += renderColumnHeader(colIdx, columns[colIdx], elemColor);
|
|
330
|
+
}
|
|
331
|
+
theadRow.insertAdjacentHTML('beforeend', newHeadersHtml);
|
|
332
|
+
|
|
333
|
+
// Add cells to each existing row (use data-row-idx for actual row index)
|
|
334
|
+
const existingRows = tbody.querySelectorAll('tr');
|
|
335
|
+
existingRows.forEach((tr) => {
|
|
336
|
+
const rowIdx = parseInt(tr.dataset.rowIdx);
|
|
337
|
+
const rowData = rows[rowIdx];
|
|
338
|
+
if (!rowData) return; // Skip if row doesn't exist
|
|
339
|
+
let newCellsHtml = '';
|
|
340
|
+
for (let colIdx = startCol; colIdx < endCol; colIdx++) {
|
|
341
|
+
const col = columns[colIdx];
|
|
342
|
+
const rawValue = rowData[colIdx];
|
|
343
|
+
const value = rawValue !== null && rawValue !== undefined ? rawValue : '';
|
|
344
|
+
const displayValue = formatCellValue(value, col.type);
|
|
345
|
+
const isColSelected = datatableSelectedColumns && datatableSelectedColumns.has(colIdx);
|
|
346
|
+
newCellsHtml += `<td data-row="${rowIdx}" data-col="${colIdx}" tabindex="0"
|
|
347
|
+
class="${isColSelected ? 'col-selected' : ''}" title="${value}" data-raw="${value}">
|
|
348
|
+
<span class="cell-text">${displayValue}</span>
|
|
349
|
+
</td>`;
|
|
350
|
+
}
|
|
351
|
+
tr.insertAdjacentHTML('beforeend', newCellsHtml);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
datatableRenderedCols = endCol;
|
|
355
|
+
datatableIsLoadingMore = false;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Auto-expand: add new empty columns to editable table when scrolling to right edge
|
|
359
|
+
function autoExpandColumns() {
|
|
360
|
+
if (!datatableData || datatableIsLoadingMore) return;
|
|
361
|
+
|
|
362
|
+
datatableIsLoadingMore = true;
|
|
363
|
+
|
|
364
|
+
const { columns, rows } = datatableData;
|
|
365
|
+
const theadRow = document.getElementById('datatable-thead-row');
|
|
366
|
+
const tbody = document.getElementById('datatable-tbody');
|
|
367
|
+
if (!theadRow || !tbody) {
|
|
368
|
+
datatableIsLoadingMore = false;
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Add COLS_PER_BATCH new columns to data
|
|
373
|
+
const startCol = columns.length;
|
|
374
|
+
for (let i = 0; i < COLS_PER_BATCH; i++) {
|
|
375
|
+
const newColIdx = columns.length;
|
|
376
|
+
columns.push({
|
|
377
|
+
name: `col${newColIdx + 1}`,
|
|
378
|
+
type: 'string',
|
|
379
|
+
index: newColIdx
|
|
380
|
+
});
|
|
381
|
+
// Add empty cell to all existing rows
|
|
382
|
+
rows.forEach(row => row.push(''));
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Add new column headers
|
|
386
|
+
let newHeadersHtml = '';
|
|
387
|
+
const elemColor = getCurrentTabElementColor();
|
|
388
|
+
for (let colIdx = startCol; colIdx < columns.length; colIdx++) {
|
|
389
|
+
newHeadersHtml += renderColumnHeader(colIdx, columns[colIdx], elemColor);
|
|
390
|
+
}
|
|
391
|
+
theadRow.insertAdjacentHTML('beforeend', newHeadersHtml);
|
|
392
|
+
|
|
393
|
+
// Add cells to each existing row
|
|
394
|
+
const existingRows = tbody.querySelectorAll('tr');
|
|
395
|
+
existingRows.forEach((tr) => {
|
|
396
|
+
const rowIdx = parseInt(tr.dataset.rowIdx);
|
|
397
|
+
const rowData = rows[rowIdx];
|
|
398
|
+
if (!rowData) return;
|
|
399
|
+
let newCellsHtml = '';
|
|
400
|
+
for (let colIdx = startCol; colIdx < columns.length; colIdx++) {
|
|
401
|
+
const col = columns[colIdx];
|
|
402
|
+
newCellsHtml += `<td data-row="${rowIdx}" data-col="${colIdx}" tabindex="0" title="" data-raw="">
|
|
403
|
+
<span class="cell-text"></span>
|
|
404
|
+
</td>`;
|
|
405
|
+
}
|
|
406
|
+
tr.insertAdjacentHTML('beforeend', newCellsHtml);
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
datatableRenderedCols = columns.length;
|
|
410
|
+
datatableIsLoadingMore = false;
|
|
411
|
+
|
|
412
|
+
// Update var assignment slots to include new columns
|
|
413
|
+
if (typeof updateVarAssignSlots === 'function') {
|
|
414
|
+
updateVarAssignSlots();
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// ============================================================================
|
|
419
|
+
// Table Editing Functions
|
|
420
|
+
// ============================================================================
|
|
421
|
+
function updateColumnName(colIdx, newName) {
|
|
422
|
+
if (!datatableData || !datatableData.columns[colIdx]) return;
|
|
423
|
+
datatableData.columns[colIdx].name = newName;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function updateColumnType(colIdx, newType) {
|
|
427
|
+
if (!datatableData || !datatableData.columns[colIdx]) return;
|
|
428
|
+
datatableData.columns[colIdx].type = newType;
|
|
429
|
+
|
|
430
|
+
// Convert existing values if changing type
|
|
431
|
+
datatableData.rows.forEach(row => {
|
|
432
|
+
if (newType === 'numeric') {
|
|
433
|
+
const val = parseFloat(row[colIdx]);
|
|
434
|
+
row[colIdx] = isNaN(val) ? 0 : val;
|
|
435
|
+
} else {
|
|
436
|
+
row[colIdx] = String(row[colIdx] || '');
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
renderEditableTable();
|
|
441
|
+
updateVarAssignSlots();
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function updateCellValue(rowIdx, colIdx, value) {
|
|
445
|
+
if (!datatableData || !datatableData.rows[rowIdx]) return;
|
|
446
|
+
|
|
447
|
+
const col = datatableData.columns[colIdx];
|
|
448
|
+
if (col.type === 'numeric') {
|
|
449
|
+
datatableData.rows[rowIdx][colIdx] = parseFloat(value) || 0;
|
|
450
|
+
} else {
|
|
451
|
+
datatableData.rows[rowIdx][colIdx] = value;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function addColumn() {
|
|
456
|
+
if (!datatableData) return;
|
|
457
|
+
|
|
458
|
+
const newColIdx = datatableData.columns.length;
|
|
459
|
+
const newColName = `col${newColIdx + 1}`;
|
|
460
|
+
|
|
461
|
+
datatableData.columns.push({
|
|
462
|
+
name: newColName,
|
|
463
|
+
type: 'numeric',
|
|
464
|
+
index: newColIdx
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
// Add empty value to all rows
|
|
468
|
+
datatableData.rows.forEach(row => {
|
|
469
|
+
row.push('');
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
renderEditableTable();
|
|
473
|
+
updateVarAssignSlots();
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function addRow() {
|
|
477
|
+
if (!datatableData) return;
|
|
478
|
+
|
|
479
|
+
// All cells start empty
|
|
480
|
+
const newRow = datatableData.columns.map(() => '');
|
|
481
|
+
datatableData.rows.push(newRow);
|
|
482
|
+
|
|
483
|
+
renderEditableTable();
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function removeLastColumn() {
|
|
487
|
+
if (!datatableData || datatableData.columns.length <= 1) return;
|
|
488
|
+
|
|
489
|
+
datatableData.columns.pop();
|
|
490
|
+
datatableData.rows.forEach(row => row.pop());
|
|
491
|
+
|
|
492
|
+
// Update column indices
|
|
493
|
+
datatableData.columns.forEach((col, idx) => {
|
|
494
|
+
col.index = idx;
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
renderEditableTable();
|
|
498
|
+
updateVarAssignSlots();
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function removeLastRow() {
|
|
502
|
+
if (!datatableData || datatableData.rows.length <= 1) return;
|
|
503
|
+
|
|
504
|
+
datatableData.rows.pop();
|
|
505
|
+
renderEditableTable();
|
|
506
|
+
}
|
|
507
|
+
"""
|
|
508
|
+
|
|
509
|
+
__all__ = ["JS_DATATABLE_EDITABLE"]
|
|
510
|
+
|
|
511
|
+
# EOF
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""Datatable import JavaScript: drag-drop, file parsing."""
|
|
4
|
+
|
|
5
|
+
JS_DATATABLE_IMPORT = """
|
|
6
|
+
// ============================================================================
|
|
7
|
+
// Drag and Drop Import
|
|
8
|
+
// ============================================================================
|
|
9
|
+
function initDatatableDropzone() {
|
|
10
|
+
const dropzone = document.getElementById('datatable-dropzone');
|
|
11
|
+
const fileInput = document.getElementById('datatable-file-input');
|
|
12
|
+
|
|
13
|
+
if (!dropzone || !fileInput) return;
|
|
14
|
+
|
|
15
|
+
// Click to select file
|
|
16
|
+
dropzone.addEventListener('click', () => fileInput.click());
|
|
17
|
+
|
|
18
|
+
// File selected
|
|
19
|
+
fileInput.addEventListener('change', (e) => {
|
|
20
|
+
if (e.target.files.length > 0) {
|
|
21
|
+
handleDataFile(e.target.files[0]);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Drag events
|
|
26
|
+
dropzone.addEventListener('dragover', (e) => {
|
|
27
|
+
e.preventDefault();
|
|
28
|
+
dropzone.classList.add('drag-over');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
dropzone.addEventListener('dragleave', () => {
|
|
32
|
+
dropzone.classList.remove('drag-over');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
dropzone.addEventListener('drop', (e) => {
|
|
36
|
+
e.preventDefault();
|
|
37
|
+
dropzone.classList.remove('drag-over');
|
|
38
|
+
|
|
39
|
+
if (e.dataTransfer.files.length > 0) {
|
|
40
|
+
handleDataFile(e.dataTransfer.files[0]);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function handleDataFile(file) {
|
|
46
|
+
const reader = new FileReader();
|
|
47
|
+
|
|
48
|
+
reader.onload = (e) => {
|
|
49
|
+
const content = e.target.result;
|
|
50
|
+
const ext = file.name.split('.').pop().toLowerCase();
|
|
51
|
+
|
|
52
|
+
if (ext === 'csv') {
|
|
53
|
+
parseCSV(content);
|
|
54
|
+
} else if (ext === 'tsv' || ext === 'txt') {
|
|
55
|
+
parseTSV(content);
|
|
56
|
+
} else if (ext === 'json') {
|
|
57
|
+
parseJSON(content);
|
|
58
|
+
} else {
|
|
59
|
+
// Try CSV by default
|
|
60
|
+
parseCSV(content);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
reader.readAsText(file);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ============================================================================
|
|
68
|
+
// Data Parsing
|
|
69
|
+
// ============================================================================
|
|
70
|
+
function parseCSV(content, delimiter = ',') {
|
|
71
|
+
const lines = content.trim().split('\\n');
|
|
72
|
+
if (lines.length === 0) return;
|
|
73
|
+
|
|
74
|
+
// Parse header
|
|
75
|
+
const headers = lines[0].split(delimiter).map(h => h.trim().replace(/^["']|["']$/g, ''));
|
|
76
|
+
|
|
77
|
+
// Parse rows
|
|
78
|
+
const rows = [];
|
|
79
|
+
for (let i = 1; i < lines.length; i++) {
|
|
80
|
+
const line = lines[i].trim();
|
|
81
|
+
if (!line) continue;
|
|
82
|
+
|
|
83
|
+
const values = line.split(delimiter).map(v => {
|
|
84
|
+
v = v.trim().replace(/^["']|["']$/g, '');
|
|
85
|
+
const num = parseFloat(v);
|
|
86
|
+
return isNaN(num) ? v : num;
|
|
87
|
+
});
|
|
88
|
+
rows.push(values);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Determine column types
|
|
92
|
+
const columns = headers.map((name, idx) => {
|
|
93
|
+
const values = rows.map(r => r[idx]).filter(v => v !== '' && v !== null && v !== undefined);
|
|
94
|
+
const isNumeric = values.every(v => typeof v === 'number');
|
|
95
|
+
return {
|
|
96
|
+
name: name,
|
|
97
|
+
type: isNumeric ? 'numeric' : 'string',
|
|
98
|
+
index: idx
|
|
99
|
+
};
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
datatableData = { columns, rows };
|
|
103
|
+
renderDatatable();
|
|
104
|
+
updateVarAssignSlots();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function parseTSV(content) {
|
|
108
|
+
parseCSV(content, '\\t');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function parseJSON(content) {
|
|
112
|
+
try {
|
|
113
|
+
const data = JSON.parse(content);
|
|
114
|
+
|
|
115
|
+
if (Array.isArray(data)) {
|
|
116
|
+
// Array of objects
|
|
117
|
+
if (data.length === 0) return;
|
|
118
|
+
|
|
119
|
+
const headers = Object.keys(data[0]);
|
|
120
|
+
const rows = data.map(obj => headers.map(h => obj[h]));
|
|
121
|
+
|
|
122
|
+
const columns = headers.map((name, idx) => {
|
|
123
|
+
const values = rows.map(r => r[idx]).filter(v => v !== '' && v !== null && v !== undefined);
|
|
124
|
+
const isNumeric = values.every(v => typeof v === 'number');
|
|
125
|
+
return { name, type: isNumeric ? 'numeric' : 'string', index: idx };
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
datatableData = { columns, rows };
|
|
129
|
+
renderDatatable();
|
|
130
|
+
updateVarAssignSlots();
|
|
131
|
+
} else if (typeof data === 'object') {
|
|
132
|
+
// Object with column arrays
|
|
133
|
+
const headers = Object.keys(data);
|
|
134
|
+
const maxLen = Math.max(...headers.map(h => Array.isArray(data[h]) ? data[h].length : 0));
|
|
135
|
+
|
|
136
|
+
const rows = [];
|
|
137
|
+
for (let i = 0; i < maxLen; i++) {
|
|
138
|
+
rows.push(headers.map(h => Array.isArray(data[h]) ? data[h][i] : data[h]));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const columns = headers.map((name, idx) => {
|
|
142
|
+
const values = rows.map(r => r[idx]).filter(v => v !== '' && v !== null && v !== undefined);
|
|
143
|
+
const isNumeric = values.every(v => typeof v === 'number');
|
|
144
|
+
return { name, type: isNumeric ? 'numeric' : 'string', index: idx };
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
datatableData = { columns, rows };
|
|
148
|
+
renderDatatable();
|
|
149
|
+
updateVarAssignSlots();
|
|
150
|
+
}
|
|
151
|
+
} catch (err) {
|
|
152
|
+
console.error('Failed to parse JSON:', err);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// loadExistingData is defined in _tabs.py to use multi-tab system
|
|
157
|
+
"""
|
|
158
|
+
|
|
159
|
+
__all__ = ["JS_DATATABLE_IMPORT"]
|
|
160
|
+
|
|
161
|
+
# EOF
|