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.
Files changed (143) hide show
  1. figrecipe/__init__.py +74 -76
  2. figrecipe/__main__.py +12 -0
  3. figrecipe/_api/_panel.py +67 -0
  4. figrecipe/_api/_save.py +100 -4
  5. figrecipe/_cli/__init__.py +7 -0
  6. figrecipe/_cli/_compose.py +87 -0
  7. figrecipe/_cli/_convert.py +117 -0
  8. figrecipe/_cli/_crop.py +82 -0
  9. figrecipe/_cli/_edit.py +70 -0
  10. figrecipe/_cli/_extract.py +128 -0
  11. figrecipe/_cli/_fonts.py +47 -0
  12. figrecipe/_cli/_info.py +67 -0
  13. figrecipe/_cli/_main.py +58 -0
  14. figrecipe/_cli/_reproduce.py +79 -0
  15. figrecipe/_cli/_style.py +77 -0
  16. figrecipe/_cli/_validate.py +66 -0
  17. figrecipe/_cli/_version.py +50 -0
  18. figrecipe/_composition/__init__.py +32 -0
  19. figrecipe/_composition/_alignment.py +452 -0
  20. figrecipe/_composition/_compose.py +179 -0
  21. figrecipe/_composition/_import_axes.py +127 -0
  22. figrecipe/_composition/_visibility.py +125 -0
  23. figrecipe/_dev/__init__.py +2 -0
  24. figrecipe/_dev/browser/__init__.py +69 -0
  25. figrecipe/_dev/browser/_audio.py +240 -0
  26. figrecipe/_dev/browser/_caption.py +356 -0
  27. figrecipe/_dev/browser/_click_effect.py +146 -0
  28. figrecipe/_dev/browser/_cursor.py +196 -0
  29. figrecipe/_dev/browser/_highlight.py +105 -0
  30. figrecipe/_dev/browser/_narration.py +237 -0
  31. figrecipe/_dev/browser/_recorder.py +446 -0
  32. figrecipe/_dev/browser/_utils.py +178 -0
  33. figrecipe/_dev/browser/_video_trim/__init__.py +152 -0
  34. figrecipe/_dev/browser/_video_trim/_detection.py +223 -0
  35. figrecipe/_dev/browser/_video_trim/_markers.py +140 -0
  36. figrecipe/_editor/__init__.py +36 -36
  37. figrecipe/_editor/_bbox/_extract.py +155 -9
  38. figrecipe/_editor/_bbox/_extract_text.py +124 -0
  39. figrecipe/_editor/_call_overrides.py +183 -0
  40. figrecipe/_editor/_datatable_plot_handlers.py +249 -0
  41. figrecipe/_editor/_figure_layout.py +211 -0
  42. figrecipe/_editor/_flask_app.py +157 -16
  43. figrecipe/_editor/_helpers.py +17 -8
  44. figrecipe/_editor/_hitmap/_detect.py +89 -32
  45. figrecipe/_editor/_hitmap_main.py +4 -4
  46. figrecipe/_editor/_overrides.py +4 -1
  47. figrecipe/_editor/_plot_types_registry.py +190 -0
  48. figrecipe/_editor/_render_overrides.py +38 -11
  49. figrecipe/_editor/_renderer.py +46 -1
  50. figrecipe/_editor/_routes_annotation.py +114 -0
  51. figrecipe/_editor/_routes_axis.py +35 -6
  52. figrecipe/_editor/_routes_captions.py +130 -0
  53. figrecipe/_editor/_routes_composition.py +270 -0
  54. figrecipe/_editor/_routes_core.py +15 -173
  55. figrecipe/_editor/_routes_datatable.py +364 -0
  56. figrecipe/_editor/_routes_element.py +37 -19
  57. figrecipe/_editor/_routes_files.py +443 -0
  58. figrecipe/_editor/_routes_image.py +200 -0
  59. figrecipe/_editor/_routes_snapshot.py +94 -0
  60. figrecipe/_editor/_routes_style.py +28 -8
  61. figrecipe/_editor/_templates/__init__.py +40 -2
  62. figrecipe/_editor/_templates/_html.py +97 -103
  63. figrecipe/_editor/_templates/_html_components/__init__.py +13 -0
  64. figrecipe/_editor/_templates/_html_components/_composition_toolbar.py +79 -0
  65. figrecipe/_editor/_templates/_html_components/_file_browser.py +41 -0
  66. figrecipe/_editor/_templates/_html_datatable.py +92 -0
  67. figrecipe/_editor/_templates/_scripts/__init__.py +58 -0
  68. figrecipe/_editor/_templates/_scripts/_accordion.py +328 -0
  69. figrecipe/_editor/_templates/_scripts/_annotation_drag.py +504 -0
  70. figrecipe/_editor/_templates/_scripts/_api.py +1 -1
  71. figrecipe/_editor/_templates/_scripts/_canvas_context_menu.py +182 -0
  72. figrecipe/_editor/_templates/_scripts/_captions.py +231 -0
  73. figrecipe/_editor/_templates/_scripts/_composition.py +283 -0
  74. figrecipe/_editor/_templates/_scripts/_core.py +94 -37
  75. figrecipe/_editor/_templates/_scripts/_datatable/__init__.py +59 -0
  76. figrecipe/_editor/_templates/_scripts/_datatable/_cell_edit.py +97 -0
  77. figrecipe/_editor/_templates/_scripts/_datatable/_clipboard.py +164 -0
  78. figrecipe/_editor/_templates/_scripts/_datatable/_context_menu.py +221 -0
  79. figrecipe/_editor/_templates/_scripts/_datatable/_core.py +150 -0
  80. figrecipe/_editor/_templates/_scripts/_datatable/_editable.py +511 -0
  81. figrecipe/_editor/_templates/_scripts/_datatable/_import.py +161 -0
  82. figrecipe/_editor/_templates/_scripts/_datatable/_plot.py +261 -0
  83. figrecipe/_editor/_templates/_scripts/_datatable/_selection.py +438 -0
  84. figrecipe/_editor/_templates/_scripts/_datatable/_table.py +256 -0
  85. figrecipe/_editor/_templates/_scripts/_datatable/_tabs.py +354 -0
  86. figrecipe/_editor/_templates/_scripts/_element_editor.py +17 -2
  87. figrecipe/_editor/_templates/_scripts/_files.py +274 -40
  88. figrecipe/_editor/_templates/_scripts/_files_context_menu.py +240 -0
  89. figrecipe/_editor/_templates/_scripts/_hitmap.py +87 -84
  90. figrecipe/_editor/_templates/_scripts/_image_drop.py +428 -0
  91. figrecipe/_editor/_templates/_scripts/_legend_drag.py +5 -0
  92. figrecipe/_editor/_templates/_scripts/_multi_select.py +198 -0
  93. figrecipe/_editor/_templates/_scripts/_panel_drag.py +219 -48
  94. figrecipe/_editor/_templates/_scripts/_panel_drag_snapshot.py +33 -0
  95. figrecipe/_editor/_templates/_scripts/_panel_position.py +238 -54
  96. figrecipe/_editor/_templates/_scripts/_panel_resize.py +230 -0
  97. figrecipe/_editor/_templates/_scripts/_panel_snap.py +307 -0
  98. figrecipe/_editor/_templates/_scripts/_region_select.py +255 -0
  99. figrecipe/_editor/_templates/_scripts/_selection.py +8 -1
  100. figrecipe/_editor/_templates/_scripts/_sync.py +242 -0
  101. figrecipe/_editor/_templates/_scripts/_undo_redo.py +348 -0
  102. figrecipe/_editor/_templates/_scripts/_zoom.py +52 -19
  103. figrecipe/_editor/_templates/_styles/__init__.py +9 -0
  104. figrecipe/_editor/_templates/_styles/_base.py +47 -0
  105. figrecipe/_editor/_templates/_styles/_buttons.py +127 -6
  106. figrecipe/_editor/_templates/_styles/_composition.py +87 -0
  107. figrecipe/_editor/_templates/_styles/_controls.py +168 -3
  108. figrecipe/_editor/_templates/_styles/_datatable/__init__.py +40 -0
  109. figrecipe/_editor/_templates/_styles/_datatable/_editable.py +203 -0
  110. figrecipe/_editor/_templates/_styles/_datatable/_panel.py +268 -0
  111. figrecipe/_editor/_templates/_styles/_datatable/_table.py +479 -0
  112. figrecipe/_editor/_templates/_styles/_datatable/_toolbar.py +384 -0
  113. figrecipe/_editor/_templates/_styles/_datatable/_vars.py +123 -0
  114. figrecipe/_editor/_templates/_styles/_dynamic_props.py +5 -5
  115. figrecipe/_editor/_templates/_styles/_file_browser.py +466 -0
  116. figrecipe/_editor/_templates/_styles/_forms.py +98 -0
  117. figrecipe/_editor/_templates/_styles/_hitmap.py +7 -0
  118. figrecipe/_editor/_templates/_styles/_modals.py +29 -0
  119. figrecipe/_editor/_templates/_styles/_overlays.py +5 -5
  120. figrecipe/_editor/_templates/_styles/_preview.py +213 -8
  121. figrecipe/_editor/_templates/_styles/_spinner.py +117 -0
  122. figrecipe/_editor/static/audio/click.mp3 +0 -0
  123. figrecipe/_editor/static/click.mp3 +0 -0
  124. figrecipe/_editor/static/icons/favicon.ico +0 -0
  125. figrecipe/_integrations/__init__.py +17 -0
  126. figrecipe/_integrations/_scitex_stats.py +298 -0
  127. figrecipe/_params/_DECORATION_METHODS.py +2 -0
  128. figrecipe/_recorder.py +28 -3
  129. figrecipe/_reproducer/_core.py +60 -49
  130. figrecipe/_utils/__init__.py +3 -0
  131. figrecipe/_utils/_bundle.py +205 -0
  132. figrecipe/_wrappers/_axes.py +150 -2
  133. figrecipe/_wrappers/_caption_generator.py +218 -0
  134. figrecipe/_wrappers/_figure.py +26 -1
  135. figrecipe/_wrappers/_stat_annotation.py +274 -0
  136. figrecipe/styles/_style_applier.py +10 -2
  137. figrecipe/styles/presets/SCITEX.yaml +11 -4
  138. {figrecipe-0.7.4.dist-info → figrecipe-0.9.0.dist-info}/METADATA +144 -146
  139. figrecipe-0.9.0.dist-info/RECORD +277 -0
  140. figrecipe-0.9.0.dist-info/entry_points.txt +2 -0
  141. figrecipe-0.7.4.dist-info/RECORD +0 -188
  142. {figrecipe-0.7.4.dist-info → figrecipe-0.9.0.dist-info}/WHEEL +0 -0
  143. {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