juxscript 1.1.203 → 1.1.205

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.
@@ -1 +1 @@
1
- {"version":3,"file":"dataframe.d.ts","sourceRoot":"","sources":["dataframe.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"dataframe.d.ts","sourceRoot":"","sources":["dataframe.ts"],"names":[],"mappings":"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"}
@@ -217,6 +217,11 @@ export class DataFrameComponent extends BaseComponent {
217
217
  }
218
218
  });
219
219
  const sheetNames = Object.keys(sheets);
220
+ if (sheetNames.length === 0) {
221
+ this._updateStatus('No data found in file', 'error');
222
+ this.state.loading = false;
223
+ return;
224
+ }
220
225
  await this._driver.store(file.name, sheets[sheetNames[0]], { source: file.name });
221
226
  if (sheetNames.length > 1) {
222
227
  this._renderMultiSheet(sheets, file.name);
@@ -251,7 +256,6 @@ export class DataFrameComponent extends BaseComponent {
251
256
  const wrapper = document.getElementById(this._id);
252
257
  if (!wrapper)
253
258
  return;
254
- // Clean up existing content
255
259
  const existingTable = wrapper.querySelector('.jux-table-wrapper');
256
260
  if (existingTable)
257
261
  existingTable.remove();
@@ -262,11 +266,10 @@ export class DataFrameComponent extends BaseComponent {
262
266
  this._sheets.set(name, df);
263
267
  });
264
268
  const sheetNames = Object.keys(sheets);
265
- // Build tabs using the Tabs component
266
269
  const tabDefs = sheetNames.map(name => ({
267
270
  id: name,
268
271
  label: name,
269
- content: '' // Content will be added after render
272
+ content: ''
270
273
  }));
271
274
  this._tabs = new Tabs(`${this._id}-tabs`, {
272
275
  tabs: tabDefs,
@@ -275,15 +278,12 @@ export class DataFrameComponent extends BaseComponent {
275
278
  this._tabs.bind('tabChange', (tabId) => {
276
279
  this._df = this._sheets.get(tabId) || null;
277
280
  });
278
- // Create container for tabs
279
281
  const tabsContainer = document.createElement('div');
280
282
  tabsContainer.className = 'jux-dataframe-tabs';
281
283
  wrapper.appendChild(tabsContainer);
282
284
  this._tabs.render(tabsContainer);
283
- // Now render tables into each tab panel
284
- sheetNames.forEach((sheetName, idx) => {
285
+ sheetNames.forEach((sheetName) => {
285
286
  const df = sheets[sheetName];
286
- const panelId = `${this._id}-tabs-${sheetName}-panel`;
287
287
  const table = new Table(`${this._id}-table-${sheetName}`, {
288
288
  striped: this._tableOptions.striped,
289
289
  hoverable: this._tableOptions.hoverable,
@@ -294,48 +294,13 @@ export class DataFrameComponent extends BaseComponent {
294
294
  });
295
295
  const columnDefs = df.columns.map(col => ({ key: col, label: col }));
296
296
  table.columns(columnDefs).rows(df.toRows());
297
- // Add settings button to tab panel
298
297
  const settingsBtn = new Button(`${this._id}-settings-${sheetName}`, {
299
298
  label: '⚙️ Import Settings',
300
299
  variant: 'ghost',
301
300
  size: 'small'
302
301
  });
303
302
  settingsBtn.bind('click', () => this._showReshapeModal());
304
- // Use addTabContent to add components
305
303
  this._tabs.addTabContent(sheetName, [settingsBtn, table]);
306
- if (this._tableOptions.filterable) {
307
- // Add filter input above table
308
- const panel = document.getElementById(panelId);
309
- if (panel) {
310
- const filterContainer = document.createElement('div');
311
- filterContainer.className = 'jux-dataframe-filter';
312
- const input = document.createElement('input');
313
- input.type = 'text';
314
- input.placeholder = `Filter ${sheetName}...`;
315
- input.className = 'jux-input-element jux-dataframe-filter-input';
316
- const iconEl = renderIcon('search');
317
- iconEl.style.width = '16px';
318
- iconEl.style.height = '16px';
319
- const iconWrap = document.createElement('span');
320
- iconWrap.className = 'jux-dataframe-filter-icon';
321
- iconWrap.appendChild(iconEl);
322
- filterContainer.appendChild(iconWrap);
323
- filterContainer.appendChild(input);
324
- input.addEventListener('input', () => {
325
- const text = input.value.toLowerCase();
326
- if (!text) {
327
- table.rows(df.toRows());
328
- return;
329
- }
330
- const filtered = df.filter((row) => Object.values(row).some(v => v !== null && v !== undefined && String(v).toLowerCase().includes(text)));
331
- table.rows(filtered.toRows());
332
- });
333
- const tableWrapper = panel.querySelector('.jux-table-wrapper');
334
- if (tableWrapper) {
335
- panel.insertBefore(filterContainer, tableWrapper);
336
- }
337
- }
338
- }
339
304
  });
340
305
  const totalRows = Object.values(sheets).reduce((sum, df) => sum + df.height, 0);
341
306
  this._updateStatus(`${sourceName} — ${sheetNames.length} sheets, ${totalRows} total rows`, 'success');
@@ -378,7 +343,6 @@ export class DataFrameComponent extends BaseComponent {
378
343
  const emptyColumns = cols.filter(c => {
379
344
  if (!c.startsWith('__EMPTY'))
380
345
  return false;
381
- // Check if ALL values in this column are null/empty
382
346
  return rows.every(row => {
383
347
  const val = row[c];
384
348
  return val === null || val === undefined || String(val).trim() === '';
@@ -411,7 +375,6 @@ export class DataFrameComponent extends BaseComponent {
411
375
  }
412
376
  else {
413
377
  this._updateStatus(`${sourceName} — ${this._df.height} rows × ${this._df.width} cols`, 'success');
414
- // Always show settings button if we have raw file data
415
378
  if (this._rawFileData) {
416
379
  requestAnimationFrame(() => {
417
380
  const statusEl = document.getElementById(`${this._id}-status`);
@@ -455,6 +418,10 @@ export class DataFrameComponent extends BaseComponent {
455
418
  }
456
419
  _detectLikelyHeaderRow(df) {
457
420
  const rows = df.toRows();
421
+ const cols = df.columns;
422
+ const colsAreGeneric = cols.some(c => c.startsWith('__EMPTY') || c.match(/^_\d+$/) || c.match(/^col_\d+$/));
423
+ if (!colsAreGeneric)
424
+ return 0;
458
425
  for (let i = 0; i < Math.min(rows.length, 10); i++) {
459
426
  const row = rows[i];
460
427
  const values = Object.values(row);
@@ -465,10 +432,8 @@ export class DataFrameComponent extends BaseComponent {
465
432
  const str = String(v).trim();
466
433
  return isNaN(Number(str)) && str !== '';
467
434
  }).length;
468
- if (nonNumericCount >= nonEmpty.length * 0.7 && i > 0) {
469
- // i is the index in toRows(). Row 0 of the file was already consumed
470
- // as the header during the initial parse, so the actual 0-based file
471
- // row index is i + 1.
435
+ if (nonNumericCount >= nonEmpty.length * 0.7) {
436
+ // toRows index i = file row (i + 1) since row 0 was used as headers
472
437
  return i + 1;
473
438
  }
474
439
  }
@@ -500,7 +465,6 @@ export class DataFrameComponent extends BaseComponent {
500
465
  if (!this._rawFileData?.file)
501
466
  return;
502
467
  this._cleanupReshapeModal();
503
- // Always detect from a fresh raw parse, not from current _df
504
468
  let suggestedRow = 0;
505
469
  try {
506
470
  const rawSheets = await this._driver.streamFileMultiSheet(this._rawFileData.file, {
@@ -526,22 +490,11 @@ export class DataFrameComponent extends BaseComponent {
526
490
  <label style="display: block; font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">
527
491
  Header Row (0-based index)
528
492
  </label>
529
- <input
530
- type="number"
531
- id="${this._id}-header-row"
532
- class="jux-input-element"
533
- value="${suggestedRow}"
534
- min="0"
535
- max="50"
536
- style="width: 100%;"
537
- />
538
- <div id="${this._id}-reshape-hint" class="jux-reshape-hint" style="margin-top: 0.5rem; padding: 0.75rem; background: hsl(var(--muted) / 0.5); border-radius: var(--radius); font-size: 0.875rem;">
539
- </div>
493
+ <input type="number" id="${this._id}-header-row" class="jux-input-element" value="${suggestedRow}" min="0" max="50" style="width: 100%;" />
494
+ <div id="${this._id}-reshape-hint" class="jux-reshape-hint" style="margin-top: 0.5rem; padding: 0.75rem; background: hsl(var(--muted) / 0.5); border-radius: var(--radius); font-size: 0.875rem;"></div>
540
495
  </div>
541
496
  <div class="jux-reshape-preview-container">
542
- <div style="font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">
543
- Preview
544
- </div>
497
+ <div style="font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">Preview</div>
545
498
  <div id="${this._id}-preview" style="font-family: ui-monospace, monospace; font-size: 12px; background: hsl(var(--muted) / 0.3); border: 1px solid hsl(var(--border)); border-radius: var(--radius); padding: 0; overflow: hidden; max-height: 400px; overflow-y: auto;"></div>
546
499
  </div>
547
500
  `;
@@ -568,6 +521,11 @@ export class DataFrameComponent extends BaseComponent {
568
521
  sheetChunkSize: this._sheetChunkSize
569
522
  });
570
523
  const sheetNames = Object.keys(sheets);
524
+ if (sheetNames.length === 0) {
525
+ this._updateStatus(`No data found with header at row ${headerRow}. Try a different row.`, 'error');
526
+ this.state.loading = false;
527
+ return;
528
+ }
571
529
  await this._driver.store(this._rawFileData.file.name, sheets[sheetNames[0]], { source: this._rawFileData.file.name });
572
530
  if (sheetNames.length > 1) {
573
531
  this._renderMultiSheet(sheets, this._rawFileData.file.name);
@@ -579,6 +537,7 @@ export class DataFrameComponent extends BaseComponent {
579
537
  }
580
538
  catch (err) {
581
539
  this._updateStatus(`Error: ${err.message}`, 'error');
540
+ this.state.loading = false;
582
541
  }
583
542
  }
584
543
  }
@@ -593,8 +552,7 @@ export class DataFrameComponent extends BaseComponent {
593
552
  if (!hintDiv)
594
553
  return;
595
554
  if (headerRow > 0) {
596
- hintDiv.innerHTML = `Row <strong>${headerRow}</strong> will be used as column headers. ` +
597
- `Rows <strong>0–${headerRow - 1}</strong> will be skipped.`;
555
+ hintDiv.innerHTML = `Row <strong>${headerRow}</strong> will be used as column headers. Rows <strong>0–${headerRow - 1}</strong> will be skipped.`;
598
556
  }
599
557
  else {
600
558
  hintDiv.innerHTML = `Row <strong>0</strong> (first row) will be used as column headers.`;
@@ -604,26 +562,46 @@ export class DataFrameComponent extends BaseComponent {
604
562
  const headerRow = parseInt(headerRowInput?.value) || 0;
605
563
  updateHint(headerRow);
606
564
  try {
607
- // ALWAYS parse with headerRow=0 to get raw file structure
608
- const rawSheets = await this._driver.streamFileMultiSheet(this._rawFileData.file, {
609
- headerRow: 0,
610
- maxSheetSize: Math.max(headerRow + 12, 15)
565
+ // Read cells directly from XLSX to match parser behavior exactly
566
+ const XLSX = await import('xlsx');
567
+ const buffer = await this._rawFileData.file.arrayBuffer();
568
+ const workbook = XLSX.read(buffer, {
569
+ type: 'array',
570
+ sheetRows: Math.max(headerRow + 12, 15),
571
+ dense: false
611
572
  });
612
- const rawSheet = Object.values(rawSheets)[0];
613
- if (!rawSheet) {
573
+ const sheetName = workbook.SheetNames[0];
574
+ const worksheet = workbook.Sheets[sheetName];
575
+ const ref = worksheet['!ref'];
576
+ if (!ref) {
614
577
  if (previewDiv)
615
578
  previewDiv.textContent = 'No data found';
616
579
  return;
617
580
  }
618
- // rawSheet.columns = row 0 values (when parsed with headerRow=0)
619
- // rawSheet.toRows() = rows 1+ (data rows when parsed with headerRow=0)
620
- const rawCols = rawSheet.columns;
621
- const rawRows = rawSheet.toRows();
622
- // Build HTML table showing raw file structure
581
+ const range = XLSX.utils.decode_range(ref);
582
+ const endRow = range.e.r;
583
+ const startCol = range.s.c;
584
+ const endCol = range.e.c;
585
+ const readCellValue = (r, c) => {
586
+ const addr = XLSX.utils.encode_cell({ r, c });
587
+ const cell = worksheet[addr];
588
+ if (!cell)
589
+ return null;
590
+ if (cell.w !== undefined)
591
+ return cell.w;
592
+ if (cell.v !== undefined)
593
+ return cell.v;
594
+ return null;
595
+ };
596
+ const readRow = (r) => {
597
+ const vals = [];
598
+ for (let c = startCol; c <= endCol; c++) {
599
+ vals.push(readCellValue(r, c));
600
+ }
601
+ return vals;
602
+ };
623
603
  let html = '<table style="width: 100%; border-collapse: collapse; font-size: 12px;">';
624
- // We need to show rows 0 through headerRow+7 (or so)
625
- // Row 0 = rawCols, Row 1+ = rawRows[i-1]
626
- const totalRowsToShow = Math.min(headerRow + 8, rawRows.length + 1);
604
+ const totalRowsToShow = Math.min(headerRow + 8, endRow + 1);
627
605
  for (let fileRow = 0; fileRow < totalRowsToShow; fileRow++) {
628
606
  const isHeader = (fileRow === headerRow);
629
607
  const isSkipped = (fileRow < headerRow);
@@ -635,7 +613,6 @@ export class DataFrameComponent extends BaseComponent {
635
613
  rowStyle += 'background: hsl(var(--muted) / 0.4); color: hsl(var(--muted-foreground)); font-style: italic; opacity: 0.7;';
636
614
  }
637
615
  html += `<tr style="${rowStyle}">`;
638
- // Row index cell
639
616
  html += `<td style="padding: 8px 12px; width: 60px; font-weight: 600; color: hsl(var(--muted-foreground)); border-right: 1px solid hsl(var(--border)); text-align: center;">`;
640
617
  if (isHeader) {
641
618
  html += `<span style="color: hsl(142 71% 45%);">▶ ${fileRow}</span>`;
@@ -644,15 +621,7 @@ export class DataFrameComponent extends BaseComponent {
644
621
  html += `${fileRow}`;
645
622
  }
646
623
  html += '</td>';
647
- // Get values for this file row
648
- let values;
649
- if (fileRow === 0) {
650
- values = rawCols;
651
- }
652
- else {
653
- values = rawRows[fileRow - 1] ? Object.values(rawRows[fileRow - 1]) : [];
654
- }
655
- // Show first 6 columns
624
+ const values = readRow(fileRow);
656
625
  const displayCols = values.slice(0, 6);
657
626
  displayCols.forEach(val => {
658
627
  const displayVal = val != null ? String(val).substring(0, 20) : '';
@@ -662,26 +631,23 @@ export class DataFrameComponent extends BaseComponent {
662
631
  html += `<td style="${cellStyle}">${this._escapeHtml(displayVal)}</td>`;
663
632
  });
664
633
  if (values.length > 6) {
665
- html += `<td style="padding: 8px 12px; color: hsl(var(--muted-foreground));">…</td>`;
634
+ html += `<td style="padding: 8px 12px; color: hsl(var(--muted-foreground));">...</td>`;
666
635
  }
667
- // Status badge cell
668
636
  html += `<td style="padding: 8px 12px; text-align: right; white-space: nowrap;">`;
669
637
  if (isHeader) {
670
- html += '<span style="background: hsl(142 71% 45%); color: white; padding: 3px 8px; border-radius: 4px; font-size: 10px; font-weight: 600;">HEADER</span>';
638
+ html += '<span style="background: hsl(var(--primary)); color: white; padding: 2px 6px; border-radius: 4px;">HEADER</span>';
671
639
  }
672
640
  else if (isSkipped) {
673
- html += '<span style="color: hsl(var(--muted-foreground)); font-size: 10px;">skipped</span>';
641
+ html += '<span style="color: hsl(var(--muted-foreground));">skipped</span>';
674
642
  }
675
643
  else {
676
- html += '<span style="color: hsl(var(--muted-foreground)); font-size: 10px;">data</span>';
644
+ html += '<span style="color: hsl(var(--success));">data</span>';
677
645
  }
678
- html += '</td>';
679
- html += '</tr>';
646
+ html += '</td></tr>';
680
647
  }
681
648
  html += '</table>';
682
- if (previewDiv) {
649
+ if (previewDiv)
683
650
  previewDiv.innerHTML = html;
684
- }
685
651
  }
686
652
  catch (err) {
687
653
  if (previewDiv)
@@ -760,6 +726,7 @@ export class DataFrameComponent extends BaseComponent {
760
726
  }
761
727
  catch (err) {
762
728
  this._updateStatus(`Error: ${err.message}`, 'error');
729
+ this.state.loading = false;
763
730
  }
764
731
  }
765
732
  }
@@ -784,8 +751,7 @@ export class DataFrameComponent extends BaseComponent {
784
751
  return;
785
752
  const headerRow = parseInt(headerRowInput?.value) || 0;
786
753
  if (headerRow > 0) {
787
- hintDiv.innerHTML = `Row <strong>${headerRow}</strong> will be used as column headers. ` +
788
- `Rows <strong>0–${headerRow - 1}</strong> will be skipped.`;
754
+ hintDiv.innerHTML = `Row <strong>${headerRow}</strong> will be used as column headers. Rows <strong>0–${headerRow - 1}</strong> will be skipped.`;
789
755
  }
790
756
  else {
791
757
  hintDiv.innerHTML = `Row <strong>0</strong> (first row) will be used as column headers.`;
@@ -798,7 +764,6 @@ export class DataFrameComponent extends BaseComponent {
798
764
  const headerRow = parseInt(headerRowInput?.value) || 0;
799
765
  updateHint();
800
766
  try {
801
- // Parse raw to show all rows
802
767
  const rawDf = this._driver.parseCSV(this._rawFileData.text, {
803
768
  delimiter: delim,
804
769
  headerRow: 0,
@@ -807,7 +772,6 @@ export class DataFrameComponent extends BaseComponent {
807
772
  });
808
773
  const rawCols = rawDf.columns;
809
774
  const rawRows = rawDf.toRows();
810
- // Build HTML table
811
775
  let html = '<table style="width: 100%; border-collapse: collapse; font-size: 11px;">';
812
776
  const totalRows = Math.min(headerRow + 8, rawRows.length + 1);
813
777
  for (let i = 0; i < totalRows; i++) {
@@ -821,11 +785,9 @@ export class DataFrameComponent extends BaseComponent {
821
785
  rowStyle += 'background: hsl(var(--muted) / 0.3); color: hsl(var(--muted-foreground)); font-style: italic;';
822
786
  }
823
787
  html += `<tr style="${rowStyle}">`;
824
- // Row index
825
788
  html += `<td style="padding: 6px 8px; width: 50px; color: hsl(var(--muted-foreground)); font-weight: 500;">`;
826
789
  html += isHeader ? `<strong>→ ${i}</strong>` : `${i}`;
827
790
  html += '</td>';
828
- // Data
829
791
  let values;
830
792
  if (i === 0) {
831
793
  values = rawCols;
@@ -843,7 +805,6 @@ export class DataFrameComponent extends BaseComponent {
843
805
  if (values.length > 6) {
844
806
  html += `<td style="padding: 6px 8px; color: hsl(var(--muted-foreground));">...</td>`;
845
807
  }
846
- // Status
847
808
  html += `<td style="padding: 6px 8px; text-align: right; font-size: 10px;">`;
848
809
  if (isHeader) {
849
810
  html += '<span style="background: hsl(var(--primary)); color: white; padding: 2px 6px; border-radius: 4px;">HEADER</span>';
@@ -854,8 +815,7 @@ export class DataFrameComponent extends BaseComponent {
854
815
  else {
855
816
  html += '<span style="color: hsl(var(--success));">data</span>';
856
817
  }
857
- html += '</td>';
858
- html += '</tr>';
818
+ html += '</td></tr>';
859
819
  }
860
820
  html += '</table>';
861
821
  if (previewDiv)
@@ -275,6 +275,13 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
275
275
  });
276
276
 
277
277
  const sheetNames = Object.keys(sheets);
278
+
279
+ if (sheetNames.length === 0) {
280
+ this._updateStatus('No data found in file', 'error');
281
+ this.state.loading = false;
282
+ return;
283
+ }
284
+
278
285
  await this._driver.store(file.name, sheets[sheetNames[0]], { source: file.name });
279
286
 
280
287
  if (sheetNames.length > 1) {
@@ -312,7 +319,6 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
312
319
  const wrapper = document.getElementById(this._id);
313
320
  if (!wrapper) return;
314
321
 
315
- // Clean up existing content
316
322
  const existingTable = wrapper.querySelector('.jux-table-wrapper');
317
323
  if (existingTable) existingTable.remove();
318
324
 
@@ -325,11 +331,10 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
325
331
 
326
332
  const sheetNames = Object.keys(sheets);
327
333
 
328
- // Build tabs using the Tabs component
329
334
  const tabDefs = sheetNames.map(name => ({
330
335
  id: name,
331
336
  label: name,
332
- content: '' // Content will be added after render
337
+ content: ''
333
338
  }));
334
339
 
335
340
  this._tabs = new Tabs(`${this._id}-tabs`, {
@@ -341,17 +346,14 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
341
346
  this._df = this._sheets.get(tabId) || null;
342
347
  });
343
348
 
344
- // Create container for tabs
345
349
  const tabsContainer = document.createElement('div');
346
350
  tabsContainer.className = 'jux-dataframe-tabs';
347
351
  wrapper.appendChild(tabsContainer);
348
352
 
349
353
  this._tabs.render(tabsContainer);
350
354
 
351
- // Now render tables into each tab panel
352
- sheetNames.forEach((sheetName, idx) => {
355
+ sheetNames.forEach((sheetName) => {
353
356
  const df = sheets[sheetName];
354
- const panelId = `${this._id}-tabs-${sheetName}-panel`;
355
357
 
356
358
  const table = new Table(`${this._id}-table-${sheetName}`, {
357
359
  striped: this._tableOptions.striped,
@@ -365,7 +367,6 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
365
367
  const columnDefs = df.columns.map(col => ({ key: col, label: col }));
366
368
  table.columns(columnDefs).rows(df.toRows());
367
369
 
368
- // Add settings button to tab panel
369
370
  const settingsBtn = new Button(`${this._id}-settings-${sheetName}`, {
370
371
  label: '⚙️ Import Settings',
371
372
  variant: 'ghost',
@@ -373,49 +374,7 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
373
374
  });
374
375
  settingsBtn.bind('click', () => this._showReshapeModal());
375
376
 
376
- // Use addTabContent to add components
377
377
  this._tabs!.addTabContent(sheetName, [settingsBtn, table]);
378
-
379
- if (this._tableOptions.filterable) {
380
- // Add filter input above table
381
- const panel = document.getElementById(panelId);
382
- if (panel) {
383
- const filterContainer = document.createElement('div');
384
- filterContainer.className = 'jux-dataframe-filter';
385
-
386
- const input = document.createElement('input');
387
- input.type = 'text';
388
- input.placeholder = `Filter ${sheetName}...`;
389
- input.className = 'jux-input-element jux-dataframe-filter-input';
390
-
391
- const iconEl = renderIcon('search');
392
- iconEl.style.width = '16px';
393
- iconEl.style.height = '16px';
394
-
395
- const iconWrap = document.createElement('span');
396
- iconWrap.className = 'jux-dataframe-filter-icon';
397
- iconWrap.appendChild(iconEl);
398
-
399
- filterContainer.appendChild(iconWrap);
400
- filterContainer.appendChild(input);
401
-
402
- input.addEventListener('input', () => {
403
- const text = input.value.toLowerCase();
404
- if (!text) { table.rows(df.toRows()); return; }
405
- const filtered = df.filter((row) =>
406
- Object.values(row).some(v =>
407
- v !== null && v !== undefined && String(v).toLowerCase().includes(text)
408
- )
409
- );
410
- table.rows(filtered.toRows());
411
- });
412
-
413
- const tableWrapper = panel.querySelector('.jux-table-wrapper');
414
- if (tableWrapper) {
415
- panel.insertBefore(filterContainer, tableWrapper);
416
- }
417
- }
418
- }
419
378
  });
420
379
 
421
380
  const totalRows = Object.values(sheets).reduce((sum, df) => sum + df.height, 0);
@@ -468,7 +427,6 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
468
427
  const rows = df.toRows();
469
428
  const emptyColumns = cols.filter(c => {
470
429
  if (!c.startsWith('__EMPTY')) return false;
471
- // Check if ALL values in this column are null/empty
472
430
  return rows.every(row => {
473
431
  const val = row[c];
474
432
  return val === null || val === undefined || String(val).trim() === '';
@@ -494,7 +452,6 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
494
452
  `${sourceName} — ${this._df!.height} rows × ${this._df!.width} cols (Data may need reformatting)`,
495
453
  'warning'
496
454
  );
497
-
498
455
  requestAnimationFrame(() => {
499
456
  const statusEl = document.getElementById(`${this._id}-status`);
500
457
  if (statusEl) {
@@ -511,8 +468,6 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
511
468
  `${sourceName} — ${this._df!.height} rows × ${this._df!.width} cols`,
512
469
  'success'
513
470
  );
514
-
515
- // Always show settings button if we have raw file data
516
471
  if (this._rawFileData) {
517
472
  requestAnimationFrame(() => {
518
473
  const statusEl = document.getElementById(`${this._id}-status`);
@@ -566,6 +521,13 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
566
521
 
567
522
  private _detectLikelyHeaderRow(df: DataFrame): number {
568
523
  const rows = df.toRows();
524
+ const cols = df.columns;
525
+
526
+ const colsAreGeneric = cols.some(c =>
527
+ c.startsWith('__EMPTY') || c.match(/^_\d+$/) || c.match(/^col_\d+$/)
528
+ );
529
+
530
+ if (!colsAreGeneric) return 0;
569
531
 
570
532
  for (let i = 0; i < Math.min(rows.length, 10); i++) {
571
533
  const row = rows[i];
@@ -579,10 +541,8 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
579
541
  return isNaN(Number(str)) && str !== '';
580
542
  }).length;
581
543
 
582
- if (nonNumericCount >= nonEmpty.length * 0.7 && i > 0) {
583
- // i is the index in toRows(). Row 0 of the file was already consumed
584
- // as the header during the initial parse, so the actual 0-based file
585
- // row index is i + 1.
544
+ if (nonNumericCount >= nonEmpty.length * 0.7) {
545
+ // toRows index i = file row (i + 1) since row 0 was used as headers
586
546
  return i + 1;
587
547
  }
588
548
  }
@@ -618,7 +578,6 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
618
578
 
619
579
  this._cleanupReshapeModal();
620
580
 
621
- // Always detect from a fresh raw parse, not from current _df
622
581
  let suggestedRow = 0;
623
582
  try {
624
583
  const rawSheets = await this._driver.streamFileMultiSheet(this._rawFileData.file, {
@@ -645,22 +604,11 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
645
604
  <label style="display: block; font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">
646
605
  Header Row (0-based index)
647
606
  </label>
648
- <input
649
- type="number"
650
- id="${this._id}-header-row"
651
- class="jux-input-element"
652
- value="${suggestedRow}"
653
- min="0"
654
- max="50"
655
- style="width: 100%;"
656
- />
657
- <div id="${this._id}-reshape-hint" class="jux-reshape-hint" style="margin-top: 0.5rem; padding: 0.75rem; background: hsl(var(--muted) / 0.5); border-radius: var(--radius); font-size: 0.875rem;">
658
- </div>
607
+ <input type="number" id="${this._id}-header-row" class="jux-input-element" value="${suggestedRow}" min="0" max="50" style="width: 100%;" />
608
+ <div id="${this._id}-reshape-hint" class="jux-reshape-hint" style="margin-top: 0.5rem; padding: 0.75rem; background: hsl(var(--muted) / 0.5); border-radius: var(--radius); font-size: 0.875rem;"></div>
659
609
  </div>
660
610
  <div class="jux-reshape-preview-container">
661
- <div style="font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">
662
- Preview
663
- </div>
611
+ <div style="font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">Preview</div>
664
612
  <div id="${this._id}-preview" style="font-family: ui-monospace, monospace; font-size: 12px; background: hsl(var(--muted) / 0.3); border: 1px solid hsl(var(--border)); border-radius: var(--radius); padding: 0; overflow: hidden; max-height: 400px; overflow-y: auto;"></div>
665
613
  </div>
666
614
  `;
@@ -691,6 +639,13 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
691
639
  });
692
640
 
693
641
  const sheetNames = Object.keys(sheets);
642
+
643
+ if (sheetNames.length === 0) {
644
+ this._updateStatus(`No data found with header at row ${headerRow}. Try a different row.`, 'error');
645
+ this.state.loading = false;
646
+ return;
647
+ }
648
+
694
649
  await this._driver.store(this._rawFileData!.file.name, sheets[sheetNames[0]], { source: this._rawFileData!.file.name });
695
650
 
696
651
  if (sheetNames.length > 1) {
@@ -702,6 +657,7 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
702
657
  this._reshapeModal!.closeModal();
703
658
  } catch (err: any) {
704
659
  this._updateStatus(`Error: ${err.message}`, 'error');
660
+ this.state.loading = false;
705
661
  }
706
662
  }
707
663
  }
@@ -719,8 +675,7 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
719
675
  const updateHint = (headerRow: number) => {
720
676
  if (!hintDiv) return;
721
677
  if (headerRow > 0) {
722
- hintDiv.innerHTML = `Row <strong>${headerRow}</strong> will be used as column headers. ` +
723
- `Rows <strong>0–${headerRow - 1}</strong> will be skipped.`;
678
+ hintDiv.innerHTML = `Row <strong>${headerRow}</strong> will be used as column headers. Rows <strong>0–${headerRow - 1}</strong> will be skipped.`;
724
679
  } else {
725
680
  hintDiv.innerHTML = `Row <strong>0</strong> (first row) will be used as column headers.`;
726
681
  }
@@ -731,36 +686,53 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
731
686
  updateHint(headerRow);
732
687
 
733
688
  try {
734
- // ALWAYS parse with headerRow=0 to get raw file structure
735
- const rawSheets = await this._driver.streamFileMultiSheet(this._rawFileData!.file, {
736
- headerRow: 0,
737
- maxSheetSize: Math.max(headerRow + 12, 15)
689
+ // Read cells directly from XLSX to match parser behavior exactly
690
+ const XLSX = await import('xlsx');
691
+ const buffer = await this._rawFileData!.file.arrayBuffer();
692
+ const workbook = XLSX.read(buffer, {
693
+ type: 'array',
694
+ sheetRows: Math.max(headerRow + 12, 15),
695
+ dense: false
738
696
  });
739
- const rawSheet = Object.values(rawSheets)[0];
740
697
 
741
- if (!rawSheet) {
698
+ const sheetName = workbook.SheetNames[0];
699
+ const worksheet = workbook.Sheets[sheetName];
700
+ const ref = worksheet['!ref'];
701
+ if (!ref) {
742
702
  if (previewDiv) previewDiv.textContent = 'No data found';
743
703
  return;
744
704
  }
745
705
 
746
- // rawSheet.columns = row 0 values (when parsed with headerRow=0)
747
- // rawSheet.toRows() = rows 1+ (data rows when parsed with headerRow=0)
748
- const rawCols = rawSheet.columns;
749
- const rawRows = rawSheet.toRows();
706
+ const range = XLSX.utils.decode_range(ref);
707
+ const endRow = range.e.r;
708
+ const startCol = range.s.c;
709
+ const endCol = range.e.c;
710
+
711
+ const readCellValue = (r: number, c: number): any => {
712
+ const addr = XLSX.utils.encode_cell({ r, c });
713
+ const cell = worksheet[addr];
714
+ if (!cell) return null;
715
+ if (cell.w !== undefined) return cell.w;
716
+ if (cell.v !== undefined) return cell.v;
717
+ return null;
718
+ };
719
+
720
+ const readRow = (r: number): any[] => {
721
+ const vals: any[] = [];
722
+ for (let c = startCol; c <= endCol; c++) {
723
+ vals.push(readCellValue(r, c));
724
+ }
725
+ return vals;
726
+ };
750
727
 
751
- // Build HTML table showing raw file structure
752
728
  let html = '<table style="width: 100%; border-collapse: collapse; font-size: 12px;">';
753
-
754
- // We need to show rows 0 through headerRow+7 (or so)
755
- // Row 0 = rawCols, Row 1+ = rawRows[i-1]
756
- const totalRowsToShow = Math.min(headerRow + 8, rawRows.length + 1);
729
+ const totalRowsToShow = Math.min(headerRow + 8, endRow + 1);
757
730
 
758
731
  for (let fileRow = 0; fileRow < totalRowsToShow; fileRow++) {
759
732
  const isHeader = (fileRow === headerRow);
760
733
  const isSkipped = (fileRow < headerRow);
761
734
 
762
735
  let rowStyle = 'border-bottom: 1px solid hsl(var(--border));';
763
-
764
736
  if (isHeader) {
765
737
  rowStyle += 'background: hsl(142 71% 45% / 0.15); font-weight: 600;';
766
738
  } else if (isSkipped) {
@@ -768,8 +740,6 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
768
740
  }
769
741
 
770
742
  html += `<tr style="${rowStyle}">`;
771
-
772
- // Row index cell
773
743
  html += `<td style="padding: 8px 12px; width: 60px; font-weight: 600; color: hsl(var(--muted-foreground)); border-right: 1px solid hsl(var(--border)); text-align: center;">`;
774
744
  if (isHeader) {
775
745
  html += `<span style="color: hsl(142 71% 45%);">▶ ${fileRow}</span>`;
@@ -778,15 +748,7 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
778
748
  }
779
749
  html += '</td>';
780
750
 
781
- // Get values for this file row
782
- let values: any[];
783
- if (fileRow === 0) {
784
- values = rawCols;
785
- } else {
786
- values = rawRows[fileRow - 1] ? Object.values(rawRows[fileRow - 1]) : [];
787
- }
788
-
789
- // Show first 6 columns
751
+ const values = readRow(fileRow);
790
752
  const displayCols = values.slice(0, 6);
791
753
  displayCols.forEach(val => {
792
754
  const displayVal = val != null ? String(val).substring(0, 20) : '';
@@ -797,28 +759,22 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
797
759
  });
798
760
 
799
761
  if (values.length > 6) {
800
- html += `<td style="padding: 8px 12px; color: hsl(var(--muted-foreground));">…</td>`;
762
+ html += `<td style="padding: 8px 12px; color: hsl(var(--muted-foreground));">...</td>`;
801
763
  }
802
764
 
803
- // Status badge cell
804
765
  html += `<td style="padding: 8px 12px; text-align: right; white-space: nowrap;">`;
805
766
  if (isHeader) {
806
- html += '<span style="background: hsl(142 71% 45%); color: white; padding: 3px 8px; border-radius: 4px; font-size: 10px; font-weight: 600;">HEADER</span>';
767
+ html += '<span style="background: hsl(var(--primary)); color: white; padding: 2px 6px; border-radius: 4px;">HEADER</span>';
807
768
  } else if (isSkipped) {
808
- html += '<span style="color: hsl(var(--muted-foreground)); font-size: 10px;">skipped</span>';
769
+ html += '<span style="color: hsl(var(--muted-foreground));">skipped</span>';
809
770
  } else {
810
- html += '<span style="color: hsl(var(--muted-foreground)); font-size: 10px;">data</span>';
771
+ html += '<span style="color: hsl(var(--success));">data</span>';
811
772
  }
812
- html += '</td>';
813
-
814
- html += '</tr>';
773
+ html += '</td></tr>';
815
774
  }
816
775
 
817
776
  html += '</table>';
818
-
819
- if (previewDiv) {
820
- previewDiv.innerHTML = html;
821
- }
777
+ if (previewDiv) previewDiv.innerHTML = html;
822
778
  } catch (err: any) {
823
779
  if (previewDiv) previewDiv.textContent = `Error: ${err.message}`;
824
780
  }
@@ -826,7 +782,6 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
826
782
 
827
783
  if (headerRowInput) headerRowInput.addEventListener('input', updatePreview);
828
784
  updatePreview();
829
-
830
785
  this._reshapeModal.open();
831
786
  }
832
787
 
@@ -901,10 +856,10 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
901
856
 
902
857
  await this._driver.store(this._rawFileData.file.name, df, { source: this._rawFileData.file.name });
903
858
  this._setDataFrame(df, this._rawFileData.file.name);
904
-
905
859
  this._reshapeModal!.closeModal();
906
860
  } catch (err: any) {
907
861
  this._updateStatus(`Error: ${err.message}`, 'error');
862
+ this.state.loading = false;
908
863
  }
909
864
  }
910
865
  }
@@ -931,8 +886,7 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
931
886
  if (!hintDiv) return;
932
887
  const headerRow = parseInt(headerRowInput?.value) || 0;
933
888
  if (headerRow > 0) {
934
- hintDiv.innerHTML = `Row <strong>${headerRow}</strong> will be used as column headers. ` +
935
- `Rows <strong>0–${headerRow - 1}</strong> will be skipped.`;
889
+ hintDiv.innerHTML = `Row <strong>${headerRow}</strong> will be used as column headers. Rows <strong>0–${headerRow - 1}</strong> will be skipped.`;
936
890
  } else {
937
891
  hintDiv.innerHTML = `Row <strong>0</strong> (first row) will be used as column headers.`;
938
892
  }
@@ -943,11 +897,9 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
943
897
 
944
898
  const delim = delimiterSelect?.value || ',';
945
899
  const headerRow = parseInt(headerRowInput?.value) || 0;
946
-
947
900
  updateHint();
948
901
 
949
902
  try {
950
- // Parse raw to show all rows
951
903
  const rawDf = this._driver.parseCSV(this._rawFileData.text, {
952
904
  delimiter: delim,
953
905
  headerRow: 0,
@@ -958,9 +910,7 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
958
910
  const rawCols = rawDf.columns;
959
911
  const rawRows = rawDf.toRows();
960
912
 
961
- // Build HTML table
962
913
  let html = '<table style="width: 100%; border-collapse: collapse; font-size: 11px;">';
963
-
964
914
  const totalRows = Math.min(headerRow + 8, rawRows.length + 1);
965
915
 
966
916
  for (let i = 0; i < totalRows; i++) {
@@ -968,7 +918,6 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
968
918
  const isSkipped = (i < headerRow);
969
919
 
970
920
  let rowStyle = 'border-bottom: 1px solid hsl(var(--border));';
971
-
972
921
  if (isHeader) {
973
922
  rowStyle += 'background: hsl(var(--primary) / 0.15); font-weight: bold;';
974
923
  } else if (isSkipped) {
@@ -976,13 +925,10 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
976
925
  }
977
926
 
978
927
  html += `<tr style="${rowStyle}">`;
979
-
980
- // Row index
981
928
  html += `<td style="padding: 6px 8px; width: 50px; color: hsl(var(--muted-foreground)); font-weight: 500;">`;
982
929
  html += isHeader ? `<strong>→ ${i}</strong>` : `${i}`;
983
930
  html += '</td>';
984
931
 
985
- // Data
986
932
  let values: any[];
987
933
  if (i === 0) {
988
934
  values = rawCols;
@@ -1001,7 +947,6 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
1001
947
  html += `<td style="padding: 6px 8px; color: hsl(var(--muted-foreground));">...</td>`;
1002
948
  }
1003
949
 
1004
- // Status
1005
950
  html += `<td style="padding: 6px 8px; text-align: right; font-size: 10px;">`;
1006
951
  if (isHeader) {
1007
952
  html += '<span style="background: hsl(var(--primary)); color: white; padding: 2px 6px; border-radius: 4px;">HEADER</span>';
@@ -1010,13 +955,10 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
1010
955
  } else {
1011
956
  html += '<span style="color: hsl(var(--success));">data</span>';
1012
957
  }
1013
- html += '</td>';
1014
-
1015
- html += '</tr>';
958
+ html += '</td></tr>';
1016
959
  }
1017
960
 
1018
961
  html += '</table>';
1019
-
1020
962
  if (previewDiv) previewDiv.innerHTML = html;
1021
963
  } catch (err: any) {
1022
964
  if (previewDiv) previewDiv.textContent = `Error: ${err.message}`;
@@ -90,6 +90,9 @@ export declare class TabularDriver {
90
90
  /**
91
91
  * ✅ FIXED: Stream Excel file with optional headerRow override
92
92
  * headerRow is 0-based: 0 = first row, 1 = second row, etc.
93
+ *
94
+ * Uses direct cell access instead of sheet_to_json to avoid
95
+ * issues with blank row handling and sparse arrays.
93
96
  */
94
97
  streamFileMultiSheet(file: File, options?: ParseOptions): Promise<Record<string, DataFrame>>;
95
98
  private _splitLines;
@@ -1 +1 @@
1
- {"version":3,"file":"TabularDriver.d.ts","sourceRoot":"","sources":["TabularDriver.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"TabularDriver.d.ts","sourceRoot":"","sources":["TabularDriver.ts"],"names":[],"mappings":"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"}
@@ -489,6 +489,9 @@ export class TabularDriver {
489
489
  /**
490
490
  * ✅ FIXED: Stream Excel file with optional headerRow override
491
491
  * headerRow is 0-based: 0 = first row, 1 = second row, etc.
492
+ *
493
+ * Uses direct cell access instead of sheet_to_json to avoid
494
+ * issues with blank row handling and sparse arrays.
492
495
  */
493
496
  async streamFileMultiSheet(file, options = {}) {
494
497
  const { maxSheetSize = 100000, sheetChunkSize = 10000, onProgress, headerRow = 0 } = options;
@@ -528,80 +531,76 @@ export class TabularDriver {
528
531
  processedSheets++;
529
532
  continue;
530
533
  }
531
- // Use sheet_to_json with header:1 to get array-of-arrays
532
- // This is the most reliable way to read all data
533
- const rawData = XLSX.utils.sheet_to_json(worksheet, {
534
- header: 1,
535
- defval: null,
536
- blankrows: true,
537
- raw: true
538
- });
539
- if (rawData.length === 0) {
540
- processedSheets++;
541
- continue;
542
- }
543
- // Ensure all rows have the same number of columns
544
- const maxCols = Math.max(endCol - startCol + 1, ...rawData.map(r => (r ? r.length : 0)));
545
- // Pad short rows with nulls
546
- for (let i = 0; i < rawData.length; i++) {
547
- if (!rawData[i]) {
548
- rawData[i] = new Array(maxCols).fill(null);
549
- }
550
- else {
551
- while (rawData[i].length < maxCols) {
552
- rawData[i].push(null);
553
- }
534
+ // Direct cell reader - bypasses sheet_to_json completely
535
+ const readCellValue = (r, c) => {
536
+ const addr = XLSX.utils.encode_cell({ r, c });
537
+ const cell = worksheet[addr];
538
+ if (!cell)
539
+ return null;
540
+ if (cell.w !== undefined)
541
+ return cell.w;
542
+ if (cell.v !== undefined)
543
+ return cell.v;
544
+ return null;
545
+ };
546
+ const readRow = (r) => {
547
+ const vals = [];
548
+ for (let c = startCol; c <= endCol; c++) {
549
+ vals.push(readCellValue(r, c));
554
550
  }
555
- }
556
- // Validate headerRow is within bounds
557
- if (headerRow >= rawData.length) {
558
- console.warn(`[TabularDriver] headerRow ${headerRow} exceeds data length ${rawData.length}`);
551
+ return vals;
552
+ };
553
+ // ✅ FIX: headerRow is the user's 0-based index counting from
554
+ // the first row of the file. Do NOT add startRow the user
555
+ // sees row 0 as the first row regardless of where the sheet
556
+ // range begins. We simply use headerRow as the absolute sheet
557
+ // row index.
558
+ const headerSheetRow = headerRow;
559
+ console.log(`[TabularDriver] Sheet "${sheetName}": range=${ref}, startRow=${startRow}, endRow=${endRow}, startCol=${startCol}, endCol=${endCol}`);
560
+ console.log(`[TabularDriver] headerRow=${headerRow}, headerSheetRow=${headerSheetRow} (absolute, no startRow offset)`);
561
+ if (headerSheetRow > endRow) {
562
+ console.warn(`[TabularDriver] headerRow ${headerRow} (sheet row ${headerSheetRow}) exceeds endRow ${endRow}`);
559
563
  processedSheets++;
560
564
  continue;
561
565
  }
562
- // Debug: log what we see at the header row
563
- console.log(`[TabularDriver] Sheet "${sheetName}" headerRow=${headerRow}, rawData.length=${rawData.length}, maxCols=${maxCols}`);
564
- console.log(`[TabularDriver] Header row data:`, rawData[headerRow]);
565
- // Extract headers from the specified row
566
- const headerRowData = rawData[headerRow];
567
- const headers = [];
568
- for (let j = 0; j < maxCols; j++) {
569
- const h = headerRowData[j];
566
+ // Read header values directly from cells
567
+ const headerValues = readRow(headerSheetRow);
568
+ console.log(`[TabularDriver] Raw header values at sheet row ${headerSheetRow}:`, headerValues);
569
+ // Build headers array
570
+ const headers = headerValues.map((h, i) => {
570
571
  if (h === null || h === undefined || String(h).trim() === '') {
571
- headers.push(`__EMPTY${j > 0 ? '_' + j : ''}`);
572
- }
573
- else {
574
- headers.push(String(h).trim());
572
+ return `__EMPTY${i > 0 ? '_' + i : ''}`;
575
573
  }
576
- }
577
- // Count valid (non-empty) headers
574
+ return String(h).trim();
575
+ });
576
+ // Count valid headers
578
577
  const validHeaders = headers.filter(h => !h.startsWith('__EMPTY'));
579
- console.log(`[TabularDriver] Headers:`, headers);
580
- console.log(`[TabularDriver] Valid headers: ${validHeaders.length}/${headers.length}`);
578
+ console.log(`[TabularDriver] Headers (${validHeaders.length} valid / ${headers.length} total):`, headers);
581
579
  if (validHeaders.length === 0) {
582
- console.warn(`[TabularDriver] No valid headers found at row ${headerRow} in sheet "${sheetName}"`);
580
+ console.warn(`[TabularDriver] No valid headers found at row ${headerRow} (sheet row ${headerSheetRow}) in sheet "${sheetName}"`);
581
+ // Log surrounding rows for debugging
582
+ for (let debugR = Math.max(startRow, headerSheetRow - 2); debugR <= Math.min(endRow, headerSheetRow + 2); debugR++) {
583
+ console.log(`[TabularDriver] row ${debugR}:`, readRow(debugR));
584
+ }
583
585
  processedSheets++;
584
586
  continue;
585
587
  }
586
- // Build rows from data AFTER the header row
588
+ // Build data rows: everything after the header row
587
589
  const rows = [];
588
- for (let i = headerRow + 1; i < rawData.length; i++) {
589
- const rowData = rawData[i];
590
+ for (let r = headerSheetRow + 1; r <= endRow; r++) {
591
+ const rowData = readRow(r);
590
592
  // Skip completely empty rows
591
- if (!rowData)
592
- continue;
593
- const hasContent = rowData.some((cell) => cell !== null && cell !== undefined && String(cell).trim() !== '');
593
+ const hasContent = rowData.some(cell => cell !== null && cell !== undefined && String(cell).trim() !== '');
594
594
  if (!hasContent)
595
595
  continue;
596
596
  const row = {};
597
597
  for (let j = 0; j < headers.length; j++) {
598
- row[headers[j]] = j < rowData.length ? rowData[j] : null;
598
+ row[headers[j]] = rowData[j];
599
599
  }
600
600
  rows.push(row);
601
601
  }
602
- console.log(`[TabularDriver] Built ${rows.length} data rows`);
602
+ console.log(`[TabularDriver] Built ${rows.length} data rows for sheet "${sheetName}"`);
603
603
  if (rows.length > 0) {
604
- console.log(`[TabularDriver] First row keys:`, Object.keys(rows[0]));
605
604
  console.log(`[TabularDriver] First row:`, rows[0]);
606
605
  }
607
606
  if (rows.length > 0) {
@@ -609,6 +609,9 @@ export class TabularDriver {
609
609
  /**
610
610
  * ✅ FIXED: Stream Excel file with optional headerRow override
611
611
  * headerRow is 0-based: 0 = first row, 1 = second row, etc.
612
+ *
613
+ * Uses direct cell access instead of sheet_to_json to avoid
614
+ * issues with blank row handling and sparse arrays.
612
615
  */
613
616
  async streamFileMultiSheet(file: File, options: ParseOptions = {}): Promise<Record<string, DataFrame>> {
614
617
  const { maxSheetSize = 100000, sheetChunkSize = 10000, onProgress, headerRow = 0 } = options;
@@ -656,91 +659,86 @@ export class TabularDriver {
656
659
  continue;
657
660
  }
658
661
 
659
- // Use sheet_to_json with header:1 to get array-of-arrays
660
- // This is the most reliable way to read all data
661
- const rawData: any[][] = XLSX.utils.sheet_to_json(worksheet, {
662
- header: 1,
663
- defval: null,
664
- blankrows: true,
665
- raw: true
666
- });
662
+ // Direct cell reader - bypasses sheet_to_json completely
663
+ const readCellValue = (r: number, c: number): any => {
664
+ const addr = XLSX.utils.encode_cell({ r, c });
665
+ const cell = worksheet[addr];
666
+ if (!cell) return null;
667
+ if (cell.w !== undefined) return cell.w;
668
+ if (cell.v !== undefined) return cell.v;
669
+ return null;
670
+ };
667
671
 
668
- if (rawData.length === 0) {
669
- processedSheets++;
670
- continue;
671
- }
672
+ const readRow = (r: number): any[] => {
673
+ const vals: any[] = [];
674
+ for (let c = startCol; c <= endCol; c++) {
675
+ vals.push(readCellValue(r, c));
676
+ }
677
+ return vals;
678
+ };
672
679
 
673
- // Ensure all rows have the same number of columns
674
- const maxCols = Math.max(endCol - startCol + 1, ...rawData.map(r => (r ? r.length : 0)));
680
+ // FIX: headerRow is the user's 0-based index counting from
681
+ // the first row of the file. Do NOT add startRow the user
682
+ // sees row 0 as the first row regardless of where the sheet
683
+ // range begins. We simply use headerRow as the absolute sheet
684
+ // row index.
685
+ const headerSheetRow = headerRow;
675
686
 
676
- // Pad short rows with nulls
677
- for (let i = 0; i < rawData.length; i++) {
678
- if (!rawData[i]) {
679
- rawData[i] = new Array(maxCols).fill(null);
680
- } else {
681
- while (rawData[i].length < maxCols) {
682
- rawData[i].push(null);
683
- }
684
- }
685
- }
687
+ console.log(`[TabularDriver] Sheet "${sheetName}": range=${ref}, startRow=${startRow}, endRow=${endRow}, startCol=${startCol}, endCol=${endCol}`);
688
+ console.log(`[TabularDriver] headerRow=${headerRow}, headerSheetRow=${headerSheetRow} (absolute, no startRow offset)`);
686
689
 
687
- // Validate headerRow is within bounds
688
- if (headerRow >= rawData.length) {
689
- console.warn(`[TabularDriver] headerRow ${headerRow} exceeds data length ${rawData.length}`);
690
+ if (headerSheetRow > endRow) {
691
+ console.warn(`[TabularDriver] headerRow ${headerRow} (sheet row ${headerSheetRow}) exceeds endRow ${endRow}`);
690
692
  processedSheets++;
691
693
  continue;
692
694
  }
693
695
 
694
- // Debug: log what we see at the header row
695
- console.log(`[TabularDriver] Sheet "${sheetName}" headerRow=${headerRow}, rawData.length=${rawData.length}, maxCols=${maxCols}`);
696
- console.log(`[TabularDriver] Header row data:`, rawData[headerRow]);
696
+ // Read header values directly from cells
697
+ const headerValues = readRow(headerSheetRow);
698
+ console.log(`[TabularDriver] Raw header values at sheet row ${headerSheetRow}:`, headerValues);
697
699
 
698
- // Extract headers from the specified row
699
- const headerRowData = rawData[headerRow];
700
- const headers: string[] = [];
701
- for (let j = 0; j < maxCols; j++) {
702
- const h = headerRowData[j];
700
+ // Build headers array
701
+ const headers: string[] = headerValues.map((h: any, i: number) => {
703
702
  if (h === null || h === undefined || String(h).trim() === '') {
704
- headers.push(`__EMPTY${j > 0 ? '_' + j : ''}`);
705
- } else {
706
- headers.push(String(h).trim());
703
+ return `__EMPTY${i > 0 ? '_' + i : ''}`;
707
704
  }
708
- }
705
+ return String(h).trim();
706
+ });
709
707
 
710
- // Count valid (non-empty) headers
708
+ // Count valid headers
711
709
  const validHeaders = headers.filter(h => !h.startsWith('__EMPTY'));
712
- console.log(`[TabularDriver] Headers:`, headers);
713
- console.log(`[TabularDriver] Valid headers: ${validHeaders.length}/${headers.length}`);
710
+ console.log(`[TabularDriver] Headers (${validHeaders.length} valid / ${headers.length} total):`, headers);
714
711
 
715
712
  if (validHeaders.length === 0) {
716
- console.warn(`[TabularDriver] No valid headers found at row ${headerRow} in sheet "${sheetName}"`);
713
+ console.warn(`[TabularDriver] No valid headers found at row ${headerRow} (sheet row ${headerSheetRow}) in sheet "${sheetName}"`);
714
+ // Log surrounding rows for debugging
715
+ for (let debugR = Math.max(startRow, headerSheetRow - 2); debugR <= Math.min(endRow, headerSheetRow + 2); debugR++) {
716
+ console.log(`[TabularDriver] row ${debugR}:`, readRow(debugR));
717
+ }
717
718
  processedSheets++;
718
719
  continue;
719
720
  }
720
721
 
721
- // Build rows from data AFTER the header row
722
+ // Build data rows: everything after the header row
722
723
  const rows: Record<string, any>[] = [];
723
- for (let i = headerRow + 1; i < rawData.length; i++) {
724
- const rowData = rawData[i];
724
+ for (let r = headerSheetRow + 1; r <= endRow; r++) {
725
+ const rowData = readRow(r);
725
726
 
726
727
  // Skip completely empty rows
727
- if (!rowData) continue;
728
-
729
- const hasContent = rowData.some((cell: any) =>
728
+ const hasContent = rowData.some(cell =>
730
729
  cell !== null && cell !== undefined && String(cell).trim() !== ''
731
730
  );
732
731
  if (!hasContent) continue;
733
732
 
734
733
  const row: Record<string, any> = {};
735
734
  for (let j = 0; j < headers.length; j++) {
736
- row[headers[j]] = j < rowData.length ? rowData[j] : null;
735
+ row[headers[j]] = rowData[j];
737
736
  }
738
737
  rows.push(row);
739
738
  }
740
739
 
741
- console.log(`[TabularDriver] Built ${rows.length} data rows`);
740
+ console.log(`[TabularDriver] Built ${rows.length} data rows for sheet "${sheetName}"`);
742
741
  if (rows.length > 0) {
743
- console.log(`[TabularDriver] First row keys:`, Object.keys(rows[0]));
744
742
  console.log(`[TabularDriver] First row:`, rows[0]);
745
743
  }
746
744
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "juxscript",
3
- "version": "1.1.203",
3
+ "version": "1.1.205",
4
4
  "type": "module",
5
5
  "description": "A JavaScript UX authorship platform",
6
6
  "main": "index.js",