juxscript 1.1.183 → 1.1.185

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.
@@ -97,7 +97,18 @@ export declare class DataFrameComponent extends BaseComponent<DataFrameState> {
97
97
  private _setDataFrame;
98
98
  private _updateTable;
99
99
  private _showFilterInput;
100
+ /**
101
+ * ✅ UPDATED: Show settings modal for CSV OR Excel reshaping
102
+ */
100
103
  private _showReshapeModal;
104
+ /**
105
+ * ✅ NEW: Excel-specific reshape modal (header row picker)
106
+ */
107
+ private _showExcelReshapeModal;
108
+ /**
109
+ * ✅ CSV-specific reshape modal (existing implementation)
110
+ */
111
+ private _showCSVReshapeModal;
101
112
  update(prop: string, value: any): void;
102
113
  render(targetId?: string | HTMLElement | BaseComponent<any>): this;
103
114
  }
@@ -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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"}
@@ -34,7 +34,7 @@ export class DataFrameComponent extends BaseComponent {
34
34
  this._sheetChunkSize = 10000; // ✅ Default 10k chunk
35
35
  this._maxFileSize = 50; // ✅ Default 50MB
36
36
  this._showReshapeWarning = true;
37
- this._rawFileData = null; // ✅ Store for re-parsing
37
+ this._rawFileData = null; // ✅ Add isExcel flag
38
38
  this._driver = new TabularDriver(options.dbName ?? 'jux-dataframes', options.storeName ?? 'frames');
39
39
  this._showStatus = options.showStatus ?? true;
40
40
  this._icon = options.icon ?? '';
@@ -104,6 +104,8 @@ export class DataFrameComponent extends BaseComponent {
104
104
  const isExcel = file.name.toLowerCase().endsWith('.xlsx') ||
105
105
  file.name.toLowerCase().endsWith('.xls');
106
106
  if (isExcel) {
107
+ // ✅ Store raw file for reshape
108
+ this._rawFileData = { file, isExcel: true };
107
109
  // ✅ Pass chunking options for large files
108
110
  const sheets = await this._driver.streamFileMultiSheet(file, {
109
111
  maxSheetSize: this._maxSheetSize,
@@ -125,7 +127,7 @@ export class DataFrameComponent extends BaseComponent {
125
127
  else {
126
128
  // ✅ CSV/TSV: Store raw text for reshaping
127
129
  const text = await file.text();
128
- this._rawFileData = { file, text };
130
+ this._rawFileData = { file, text, isExcel: false };
129
131
  const df = this._driver.parseCSV(text, {
130
132
  autoDetectDelimiter: true,
131
133
  hasHeader: true
@@ -393,8 +395,8 @@ export class DataFrameComponent extends BaseComponent {
393
395
  }
394
396
  this._updateTable();
395
397
  this._updateStatus(`${sourceName} — ${this._df.height} rows × ${this._df.width} cols`, 'success');
396
- // ✅ Add reshape warning button if CSV and enabled
397
- if (this._showReshapeWarning && this._rawFileData?.text) {
398
+ // ✅ FIXED: Add reshape warning button if CSV OR Excel and enabled
399
+ if (this._showReshapeWarning && this._rawFileData) {
398
400
  const statusEl = document.getElementById(`${this._id}-status`);
399
401
  if (statusEl) {
400
402
  const settingsBtn = document.createElement('button');
@@ -481,9 +483,230 @@ export class DataFrameComponent extends BaseComponent {
481
483
  this._table?.rows(filtered.toRows());
482
484
  });
483
485
  }
486
+ /**
487
+ * ✅ UPDATED: Show settings modal for CSV OR Excel reshaping
488
+ */
484
489
  _showReshapeModal() {
485
- // TODO: Implement reshape modal for re-parsing CSV with different options
486
- console.warn('Reshape modal not yet implemented');
490
+ if (!this._rawFileData)
491
+ return;
492
+ const isExcel = this._rawFileData.isExcel;
493
+ if (isExcel) {
494
+ // ✅ Excel-specific modal: Let user pick header row from sheet
495
+ this._showExcelReshapeModal();
496
+ }
497
+ else {
498
+ // ✅ CSV-specific modal: delimiter + header row
499
+ this._showCSVReshapeModal();
500
+ }
501
+ }
502
+ /**
503
+ * ✅ NEW: Excel-specific reshape modal (header row picker)
504
+ */
505
+ async _showExcelReshapeModal() {
506
+ if (!this._rawFileData?.file)
507
+ return;
508
+ const modal = document.createElement('div');
509
+ modal.className = 'jux-modal-overlay';
510
+ modal.innerHTML = `
511
+ <div class="jux-modal" style="max-width: 800px;">
512
+ <div class="jux-modal-header">
513
+ <div class="jux-modal-header-title">Excel Import Settings</div>
514
+ <button class="jux-modal-close" id="${this._id}-modal-close">×</button>
515
+ </div>
516
+ <div class="jux-modal-content" style="padding: 1.5rem;">
517
+ <div style="margin-bottom: 1rem;">
518
+ <label style="display: block; font-weight: 600; margin-bottom: 0.5rem;">Header Row (0-based)</label>
519
+ <input type="number" id="${this._id}-header-row" class="jux-input-element" value="0" min="0" max="50" style="width: 100%;" />
520
+ <div style="font-size: 0.875rem; color: hsl(var(--muted-foreground)); margin-top: 0.5rem;">
521
+ ⚠️ Detected: Row 0 has "Exported On:", row 2 has "340B ID". Try setting this to 2.
522
+ </div>
523
+ </div>
524
+
525
+ <div style="border: 1px solid hsl(var(--border)); border-radius: var(--radius); padding: 1rem; background: hsl(var(--muted) / 0.3); max-height: 300px; overflow-y: auto;">
526
+ <div style="font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">Preview (first 10 rows)</div>
527
+ <div id="${this._id}-preview" style="font-family: monospace; font-size: 0.75rem; white-space: pre; color: hsl(var(--muted-foreground));"></div>
528
+ </div>
529
+ </div>
530
+ <div class="jux-modal-footer">
531
+ <button id="${this._id}-cancel-reshape" class="jux-button jux-button-ghost">Cancel</button>
532
+ <button id="${this._id}-apply-reshape" class="jux-button">Apply & Re-import</button>
533
+ </div>
534
+ </div>
535
+ `;
536
+ document.body.appendChild(modal);
537
+ const headerRowInput = document.getElementById(`${this._id}-header-row`);
538
+ const previewDiv = document.getElementById(`${this._id}-preview`);
539
+ // ✅ Update preview on header row change
540
+ const updatePreview = async () => {
541
+ const headerRow = parseInt(headerRowInput.value) || 0;
542
+ try {
543
+ // Re-parse Excel with specified header row
544
+ const sheets = await this._driver.streamFileMultiSheet(this._rawFileData.file, {
545
+ headerRow, // ✅ Pass header row to parser
546
+ maxSheetSize: 10 // Preview only
547
+ });
548
+ const firstSheet = Object.values(sheets)[0];
549
+ if (!firstSheet) {
550
+ previewDiv.textContent = '⚠️ No data found';
551
+ return;
552
+ }
553
+ const preview = firstSheet.toRows().slice(0, 10).map((row, i) => {
554
+ const cols = Object.values(row).map(v => String(v).padEnd(20)).join(' | ');
555
+ return `${i === 0 ? '📌 ' : ' '}${cols}`;
556
+ }).join('\n');
557
+ previewDiv.textContent = `Columns: ${firstSheet.columns.join(' | ')}\n\n${preview}`;
558
+ }
559
+ catch (err) {
560
+ previewDiv.textContent = `⚠️ Error: ${err.message}`;
561
+ }
562
+ };
563
+ headerRowInput.addEventListener('input', updatePreview);
564
+ updatePreview();
565
+ // ✅ Apply button
566
+ document.getElementById(`${this._id}-apply-reshape`).addEventListener('click', async () => {
567
+ const headerRow = parseInt(headerRowInput.value) || 0;
568
+ this.state.loading = true;
569
+ this._updateStatus('⏳ Re-parsing with new settings...', 'loading');
570
+ try {
571
+ const sheets = await this._driver.streamFileMultiSheet(this._rawFileData.file, {
572
+ headerRow,
573
+ maxSheetSize: this._maxSheetSize,
574
+ sheetChunkSize: this._sheetChunkSize
575
+ });
576
+ const sheetNames = Object.keys(sheets);
577
+ await this._driver.store(this._rawFileData.file.name, sheets[sheetNames[0]], { source: this._rawFileData.file.name });
578
+ if (sheetNames.length > 1) {
579
+ this._renderMultiSheet(sheets, this._rawFileData.file.name);
580
+ }
581
+ else {
582
+ this._setDataFrame(sheets[sheetNames[0]], this._rawFileData.file.name);
583
+ }
584
+ modal.remove();
585
+ }
586
+ catch (err) {
587
+ this._updateStatus(`❌ ${err.message}`, 'error');
588
+ }
589
+ });
590
+ // ✅ Cancel/Close buttons
591
+ const closeModal = () => modal.remove();
592
+ document.getElementById(`${this._id}-cancel-reshape`).addEventListener('click', closeModal);
593
+ document.getElementById(`${this._id}-modal-close`).addEventListener('click', closeModal);
594
+ }
595
+ /**
596
+ * ✅ CSV-specific reshape modal (existing implementation)
597
+ */
598
+ _showCSVReshapeModal() {
599
+ if (!this._rawFileData)
600
+ return;
601
+ const modal = document.createElement('div');
602
+ modal.className = 'jux-modal-overlay';
603
+ modal.innerHTML = `
604
+ <div class="jux-modal" style="max-width: 800px;">
605
+ <div class="jux-modal-header">
606
+ <div class="jux-modal-header-title">Data Import Settings</div>
607
+ <button class="jux-modal-close" id="${this._id}-modal-close">×</button>
608
+ </div>
609
+ <div class="jux-modal-content" style="padding: 1.5rem;">
610
+ <div style="margin-bottom: 1rem;">
611
+ <label style="display: block; font-weight: 600; margin-bottom: 0.5rem;">Delimiter</label>
612
+ <select id="${this._id}-delimiter" class="jux-input-element" style="width: 100%;">
613
+ <option value=",">Comma (,)</option>
614
+ <option value="|">Pipe (|)</option>
615
+ <option value="\t">Tab (\\t)</option>
616
+ <option value=";">Semicolon (;)</option>
617
+ </select>
618
+ </div>
619
+
620
+ <div style="margin-bottom: 1rem;">
621
+ <label style="display: block; font-weight: 600; margin-bottom: 0.5rem;">Header Row (0-based)</label>
622
+ <input type="number" id="${this._id}-header-row" class="jux-input-element" value="0" min="0" max="50" style="width: 100%;" />
623
+ </div>
624
+
625
+ <div style="margin-bottom: 1rem;">
626
+ <label style="display: block; font-weight: 600; margin-bottom: 0.5rem;">Skip Rows Before Header</label>
627
+ <input type="number" id="${this._id}-skip-rows" class="jux-input-element" value="0" min="0" max="50" style="width: 100%;" />
628
+ </div>
629
+
630
+ <div style="border: 1px solid hsl(var(--border)); border-radius: var(--radius); padding: 1rem; background: hsl(var(--muted) / 0.3); max-height: 300px; overflow-y: auto;">
631
+ <div style="font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">Preview</div>
632
+ <div id="${this._id}-preview" style="font-family: monospace; font-size: 0.75rem; white-space: pre; color: hsl(var(--muted-foreground));"></div>
633
+ </div>
634
+ </div>
635
+ <div class="jux-modal-footer">
636
+ <button id="${this._id}-cancel-reshape" class="jux-button jux-button-ghost">Cancel</button>
637
+ <button id="${this._id}-apply-reshape" class="jux-button">Apply & Re-import</button>
638
+ </div>
639
+ </div>
640
+ `;
641
+ document.body.appendChild(modal);
642
+ const delimiterSelect = document.getElementById(`${this._id}-delimiter`);
643
+ const headerRowInput = document.getElementById(`${this._id}-header-row`);
644
+ const skipRowsInput = document.getElementById(`${this._id}-skip-rows`);
645
+ const previewDiv = document.getElementById(`${this._id}-preview`);
646
+ // ✅ Auto-detect initial values
647
+ if (this._rawFileData.text) {
648
+ const detected = this._driver._detectDelimiter(this._rawFileData.text);
649
+ delimiterSelect.value = detected === '\t' ? '\\t' : detected;
650
+ const headerRow = this._driver._detectHeaderRow(this._rawFileData.text, detected);
651
+ headerRowInput.value = String(headerRow);
652
+ }
653
+ // ✅ Update preview on changes
654
+ const updatePreview = async () => {
655
+ if (!this._rawFileData?.text)
656
+ return;
657
+ const delim = delimiterSelect.value === '\\t' ? '\t' : delimiterSelect.value;
658
+ const headerRow = parseInt(headerRowInput.value) || 0;
659
+ const skipRows = parseInt(skipRowsInput.value) || 0;
660
+ try {
661
+ const df = this._driver.parseCSV(this._rawFileData.text, {
662
+ delimiter: delim,
663
+ headerRow,
664
+ skipRows,
665
+ hasHeader: true,
666
+ maxRows: 10 // Preview first 10 rows
667
+ });
668
+ const preview = df.toRows().map((row, i) => {
669
+ const cols = Object.values(row).map(v => String(v).padEnd(20)).join(' | ');
670
+ return `${i === 0 ? '📌 ' : ' '}${cols}`;
671
+ }).join('\n');
672
+ previewDiv.textContent = `Columns: ${df.columns.join(' | ')}\n\n${preview}`;
673
+ }
674
+ catch (err) {
675
+ previewDiv.textContent = `⚠️ Error: ${err.message}`;
676
+ }
677
+ };
678
+ delimiterSelect.addEventListener('change', updatePreview);
679
+ headerRowInput.addEventListener('input', updatePreview);
680
+ skipRowsInput.addEventListener('input', updatePreview);
681
+ updatePreview();
682
+ // ✅ Apply button
683
+ document.getElementById(`${this._id}-apply-reshape`).addEventListener('click', async () => {
684
+ if (!this._rawFileData?.text)
685
+ return;
686
+ const delim = delimiterSelect.value === '\\t' ? '\t' : delimiterSelect.value;
687
+ const headerRow = parseInt(headerRowInput.value) || 0;
688
+ const skipRows = parseInt(skipRowsInput.value) || 0;
689
+ this.state.loading = true;
690
+ this._updateStatus('⏳ Re-parsing with new settings...', 'loading');
691
+ try {
692
+ const df = this._driver.parseCSV(this._rawFileData.text, {
693
+ delimiter: delim,
694
+ headerRow,
695
+ skipRows,
696
+ hasHeader: true
697
+ });
698
+ await this._driver.store(this._rawFileData.file.name, df, { source: this._rawFileData.file.name });
699
+ this._setDataFrame(df, this._rawFileData.file.name);
700
+ modal.remove();
701
+ }
702
+ catch (err) {
703
+ this._updateStatus(`❌ ${err.message}`, 'error');
704
+ }
705
+ });
706
+ // ✅ Cancel/Close buttons
707
+ const closeModal = () => modal.remove();
708
+ document.getElementById(`${this._id}-cancel-reshape`).addEventListener('click', closeModal);
709
+ document.getElementById(`${this._id}-modal-close`).addEventListener('click', closeModal);
487
710
  }
488
711
  update(prop, value) { }
489
712
  /* ═══════════════════════════════════════════════════
@@ -521,6 +744,8 @@ export class DataFrameComponent extends BaseComponent {
521
744
  const isExcel = file.name.toLowerCase().endsWith('.xlsx') ||
522
745
  file.name.toLowerCase().endsWith('.xls');
523
746
  if (isExcel) {
747
+ // ✅ Store raw file for reshape
748
+ this._rawFileData = { file, isExcel: true };
524
749
  const sheets = await this._driver.streamFileMultiSheet(file, {
525
750
  maxSheetSize: this._maxSheetSize,
526
751
  sheetChunkSize: this._sheetChunkSize,
@@ -59,7 +59,7 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
59
59
  private _sheetChunkSize: number = 10000; // ✅ Default 10k chunk
60
60
  private _maxFileSize: number = 50; // ✅ Default 50MB
61
61
  private _showReshapeWarning: boolean = true;
62
- private _rawFileData: { file: File; text?: string } | null = null; // ✅ Store for re-parsing
62
+ private _rawFileData: { file: File; text?: string; isExcel?: boolean } | null = null; // ✅ Add isExcel flag
63
63
 
64
64
  constructor(id: string, options: DataFrameOptions = {}) {
65
65
  super(id, {
@@ -149,6 +149,9 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
149
149
  file.name.toLowerCase().endsWith('.xls');
150
150
 
151
151
  if (isExcel) {
152
+ // ✅ Store raw file for reshape
153
+ this._rawFileData = { file, isExcel: true };
154
+
152
155
  // ✅ Pass chunking options for large files
153
156
  const sheets = await this._driver.streamFileMultiSheet(file, {
154
157
  maxSheetSize: this._maxSheetSize,
@@ -171,7 +174,7 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
171
174
  } else {
172
175
  // ✅ CSV/TSV: Store raw text for reshaping
173
176
  const text = await file.text();
174
- this._rawFileData = { file, text };
177
+ this._rawFileData = { file, text, isExcel: false };
175
178
 
176
179
  const df = this._driver.parseCSV(text, {
177
180
  autoDetectDelimiter: true,
@@ -493,8 +496,8 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
493
496
  'success'
494
497
  );
495
498
 
496
- // ✅ Add reshape warning button if CSV and enabled
497
- if (this._showReshapeWarning && this._rawFileData?.text) {
499
+ // ✅ FIXED: Add reshape warning button if CSV OR Excel and enabled
500
+ if (this._showReshapeWarning && this._rawFileData) {
498
501
  const statusEl = document.getElementById(`${this._id}-status`);
499
502
  if (statusEl) {
500
503
  const settingsBtn = document.createElement('button');
@@ -596,9 +599,258 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
596
599
  });
597
600
  }
598
601
 
602
+ /**
603
+ * ✅ UPDATED: Show settings modal for CSV OR Excel reshaping
604
+ */
599
605
  private _showReshapeModal(): void {
600
- // TODO: Implement reshape modal for re-parsing CSV with different options
601
- console.warn('Reshape modal not yet implemented');
606
+ if (!this._rawFileData) return;
607
+
608
+ const isExcel = this._rawFileData.isExcel;
609
+
610
+ if (isExcel) {
611
+ // ✅ Excel-specific modal: Let user pick header row from sheet
612
+ this._showExcelReshapeModal();
613
+ } else {
614
+ // ✅ CSV-specific modal: delimiter + header row
615
+ this._showCSVReshapeModal();
616
+ }
617
+ }
618
+
619
+ /**
620
+ * ✅ NEW: Excel-specific reshape modal (header row picker)
621
+ */
622
+ private async _showExcelReshapeModal(): Promise<void> {
623
+ if (!this._rawFileData?.file) return;
624
+
625
+ const modal = document.createElement('div');
626
+ modal.className = 'jux-modal-overlay';
627
+ modal.innerHTML = `
628
+ <div class="jux-modal" style="max-width: 800px;">
629
+ <div class="jux-modal-header">
630
+ <div class="jux-modal-header-title">Excel Import Settings</div>
631
+ <button class="jux-modal-close" id="${this._id}-modal-close">×</button>
632
+ </div>
633
+ <div class="jux-modal-content" style="padding: 1.5rem;">
634
+ <div style="margin-bottom: 1rem;">
635
+ <label style="display: block; font-weight: 600; margin-bottom: 0.5rem;">Header Row (0-based)</label>
636
+ <input type="number" id="${this._id}-header-row" class="jux-input-element" value="0" min="0" max="50" style="width: 100%;" />
637
+ <div style="font-size: 0.875rem; color: hsl(var(--muted-foreground)); margin-top: 0.5rem;">
638
+ ⚠️ Detected: Row 0 has "Exported On:", row 2 has "340B ID". Try setting this to 2.
639
+ </div>
640
+ </div>
641
+
642
+ <div style="border: 1px solid hsl(var(--border)); border-radius: var(--radius); padding: 1rem; background: hsl(var(--muted) / 0.3); max-height: 300px; overflow-y: auto;">
643
+ <div style="font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">Preview (first 10 rows)</div>
644
+ <div id="${this._id}-preview" style="font-family: monospace; font-size: 0.75rem; white-space: pre; color: hsl(var(--muted-foreground));"></div>
645
+ </div>
646
+ </div>
647
+ <div class="jux-modal-footer">
648
+ <button id="${this._id}-cancel-reshape" class="jux-button jux-button-ghost">Cancel</button>
649
+ <button id="${this._id}-apply-reshape" class="jux-button">Apply & Re-import</button>
650
+ </div>
651
+ </div>
652
+ `;
653
+
654
+ document.body.appendChild(modal);
655
+
656
+ const headerRowInput = document.getElementById(`${this._id}-header-row`) as HTMLInputElement;
657
+ const previewDiv = document.getElementById(`${this._id}-preview`)!;
658
+
659
+ // ✅ Update preview on header row change
660
+ const updatePreview = async () => {
661
+ const headerRow = parseInt(headerRowInput.value) || 0;
662
+
663
+ try {
664
+ // Re-parse Excel with specified header row
665
+ const sheets = await this._driver.streamFileMultiSheet(this._rawFileData!.file, {
666
+ headerRow, // ✅ Pass header row to parser
667
+ maxSheetSize: 10 // Preview only
668
+ });
669
+
670
+ const firstSheet = Object.values(sheets)[0];
671
+ if (!firstSheet) {
672
+ previewDiv.textContent = '⚠️ No data found';
673
+ return;
674
+ }
675
+
676
+ const preview = firstSheet.toRows().slice(0, 10).map((row, i) => {
677
+ const cols = Object.values(row).map(v => String(v).padEnd(20)).join(' | ');
678
+ return `${i === 0 ? '📌 ' : ' '}${cols}`;
679
+ }).join('\n');
680
+
681
+ previewDiv.textContent = `Columns: ${firstSheet.columns.join(' | ')}\n\n${preview}`;
682
+ } catch (err: any) {
683
+ previewDiv.textContent = `⚠️ Error: ${err.message}`;
684
+ }
685
+ };
686
+
687
+ headerRowInput.addEventListener('input', updatePreview);
688
+ updatePreview();
689
+
690
+ // ✅ Apply button
691
+ document.getElementById(`${this._id}-apply-reshape`)!.addEventListener('click', async () => {
692
+ const headerRow = parseInt(headerRowInput.value) || 0;
693
+
694
+ this.state.loading = true;
695
+ this._updateStatus('⏳ Re-parsing with new settings...', 'loading');
696
+
697
+ try {
698
+ const sheets = await this._driver.streamFileMultiSheet(this._rawFileData!.file, {
699
+ headerRow,
700
+ maxSheetSize: this._maxSheetSize,
701
+ sheetChunkSize: this._sheetChunkSize
702
+ });
703
+
704
+ const sheetNames = Object.keys(sheets);
705
+ await this._driver.store(this._rawFileData!.file.name, sheets[sheetNames[0]], { source: this._rawFileData!.file.name });
706
+
707
+ if (sheetNames.length > 1) {
708
+ this._renderMultiSheet(sheets, this._rawFileData!.file.name);
709
+ } else {
710
+ this._setDataFrame(sheets[sheetNames[0]], this._rawFileData!.file.name);
711
+ }
712
+
713
+ modal.remove();
714
+ } catch (err: any) {
715
+ this._updateStatus(`❌ ${err.message}`, 'error');
716
+ }
717
+ });
718
+
719
+ // ✅ Cancel/Close buttons
720
+ const closeModal = () => modal.remove();
721
+ document.getElementById(`${this._id}-cancel-reshape`)!.addEventListener('click', closeModal);
722
+ document.getElementById(`${this._id}-modal-close`)!.addEventListener('click', closeModal);
723
+ }
724
+
725
+ /**
726
+ * ✅ CSV-specific reshape modal (existing implementation)
727
+ */
728
+ private _showCSVReshapeModal(): void {
729
+ if (!this._rawFileData) return;
730
+
731
+ const modal = document.createElement('div');
732
+ modal.className = 'jux-modal-overlay';
733
+ modal.innerHTML = `
734
+ <div class="jux-modal" style="max-width: 800px;">
735
+ <div class="jux-modal-header">
736
+ <div class="jux-modal-header-title">Data Import Settings</div>
737
+ <button class="jux-modal-close" id="${this._id}-modal-close">×</button>
738
+ </div>
739
+ <div class="jux-modal-content" style="padding: 1.5rem;">
740
+ <div style="margin-bottom: 1rem;">
741
+ <label style="display: block; font-weight: 600; margin-bottom: 0.5rem;">Delimiter</label>
742
+ <select id="${this._id}-delimiter" class="jux-input-element" style="width: 100%;">
743
+ <option value=",">Comma (,)</option>
744
+ <option value="|">Pipe (|)</option>
745
+ <option value="\t">Tab (\\t)</option>
746
+ <option value=";">Semicolon (;)</option>
747
+ </select>
748
+ </div>
749
+
750
+ <div style="margin-bottom: 1rem;">
751
+ <label style="display: block; font-weight: 600; margin-bottom: 0.5rem;">Header Row (0-based)</label>
752
+ <input type="number" id="${this._id}-header-row" class="jux-input-element" value="0" min="0" max="50" style="width: 100%;" />
753
+ </div>
754
+
755
+ <div style="margin-bottom: 1rem;">
756
+ <label style="display: block; font-weight: 600; margin-bottom: 0.5rem;">Skip Rows Before Header</label>
757
+ <input type="number" id="${this._id}-skip-rows" class="jux-input-element" value="0" min="0" max="50" style="width: 100%;" />
758
+ </div>
759
+
760
+ <div style="border: 1px solid hsl(var(--border)); border-radius: var(--radius); padding: 1rem; background: hsl(var(--muted) / 0.3); max-height: 300px; overflow-y: auto;">
761
+ <div style="font-weight: 600; margin-bottom: 0.5rem; color: hsl(var(--foreground));">Preview</div>
762
+ <div id="${this._id}-preview" style="font-family: monospace; font-size: 0.75rem; white-space: pre; color: hsl(var(--muted-foreground));"></div>
763
+ </div>
764
+ </div>
765
+ <div class="jux-modal-footer">
766
+ <button id="${this._id}-cancel-reshape" class="jux-button jux-button-ghost">Cancel</button>
767
+ <button id="${this._id}-apply-reshape" class="jux-button">Apply & Re-import</button>
768
+ </div>
769
+ </div>
770
+ `;
771
+
772
+ document.body.appendChild(modal);
773
+
774
+ const delimiterSelect = document.getElementById(`${this._id}-delimiter`) as HTMLSelectElement;
775
+ const headerRowInput = document.getElementById(`${this._id}-header-row`) as HTMLInputElement;
776
+ const skipRowsInput = document.getElementById(`${this._id}-skip-rows`) as HTMLInputElement;
777
+ const previewDiv = document.getElementById(`${this._id}-preview`)!;
778
+
779
+ // ✅ Auto-detect initial values
780
+ if (this._rawFileData.text) {
781
+ const detected = (this._driver as any)._detectDelimiter(this._rawFileData.text);
782
+ delimiterSelect.value = detected === '\t' ? '\\t' : detected;
783
+
784
+ const headerRow = (this._driver as any)._detectHeaderRow(this._rawFileData.text, detected);
785
+ headerRowInput.value = String(headerRow);
786
+ }
787
+
788
+ // ✅ Update preview on changes
789
+ const updatePreview = async () => {
790
+ if (!this._rawFileData?.text) return;
791
+
792
+ const delim = delimiterSelect.value === '\\t' ? '\t' : delimiterSelect.value;
793
+ const headerRow = parseInt(headerRowInput.value) || 0;
794
+ const skipRows = parseInt(skipRowsInput.value) || 0;
795
+
796
+ try {
797
+ const df = this._driver.parseCSV(this._rawFileData.text, {
798
+ delimiter: delim,
799
+ headerRow,
800
+ skipRows,
801
+ hasHeader: true,
802
+ maxRows: 10 // Preview first 10 rows
803
+ });
804
+
805
+ const preview = df.toRows().map((row, i) => {
806
+ const cols = Object.values(row).map(v => String(v).padEnd(20)).join(' | ');
807
+ return `${i === 0 ? '📌 ' : ' '}${cols}`;
808
+ }).join('\n');
809
+
810
+ previewDiv.textContent = `Columns: ${df.columns.join(' | ')}\n\n${preview}`;
811
+ } catch (err: any) {
812
+ previewDiv.textContent = `⚠️ Error: ${err.message}`;
813
+ }
814
+ };
815
+
816
+ delimiterSelect.addEventListener('change', updatePreview);
817
+ headerRowInput.addEventListener('input', updatePreview);
818
+ skipRowsInput.addEventListener('input', updatePreview);
819
+
820
+ updatePreview();
821
+
822
+ // ✅ Apply button
823
+ document.getElementById(`${this._id}-apply-reshape`)!.addEventListener('click', async () => {
824
+ if (!this._rawFileData?.text) return;
825
+
826
+ const delim = delimiterSelect.value === '\\t' ? '\t' : delimiterSelect.value;
827
+ const headerRow = parseInt(headerRowInput.value) || 0;
828
+ const skipRows = parseInt(skipRowsInput.value) || 0;
829
+
830
+ this.state.loading = true;
831
+ this._updateStatus('⏳ Re-parsing with new settings...', 'loading');
832
+
833
+ try {
834
+ const df = this._driver.parseCSV(this._rawFileData.text, {
835
+ delimiter: delim,
836
+ headerRow,
837
+ skipRows,
838
+ hasHeader: true
839
+ });
840
+
841
+ await this._driver.store(this._rawFileData.file.name, df, { source: this._rawFileData.file.name });
842
+ this._setDataFrame(df, this._rawFileData.file.name);
843
+
844
+ modal.remove();
845
+ } catch (err: any) {
846
+ this._updateStatus(`❌ ${err.message}`, 'error');
847
+ }
848
+ });
849
+
850
+ // ✅ Cancel/Close buttons
851
+ const closeModal = () => modal.remove();
852
+ document.getElementById(`${this._id}-cancel-reshape`)!.addEventListener('click', closeModal);
853
+ document.getElementById(`${this._id}-modal-close`)!.addEventListener('click', closeModal);
602
854
  }
603
855
 
604
856
  update(prop: string, value: any): void { }
@@ -642,6 +894,9 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
642
894
  file.name.toLowerCase().endsWith('.xls');
643
895
 
644
896
  if (isExcel) {
897
+ // ✅ Store raw file for reshape
898
+ this._rawFileData = { file, isExcel: true };
899
+
645
900
  const sheets = await this._driver.streamFileMultiSheet(file, {
646
901
  maxSheetSize: this._maxSheetSize,
647
902
  sheetChunkSize: this._sheetChunkSize,
@@ -88,8 +88,7 @@ export declare class TabularDriver {
88
88
  */
89
89
  fetch(url: string, options?: ParseOptions): Promise<DataFrame>;
90
90
  /**
91
- * ✅ OPTIMIZED: Stream Excel file with chunked row processing
92
- * Handles large files by processing rows in batches
91
+ * ✅ UPDATED: Stream Excel file with optional headerRow override
93
92
  */
94
93
  streamFileMultiSheet(file: File, options?: ParseOptions): Promise<Record<string, DataFrame>>;
95
94
  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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"}
@@ -487,11 +487,10 @@ export class TabularDriver {
487
487
  return df;
488
488
  }
489
489
  /**
490
- * ✅ OPTIMIZED: Stream Excel file with chunked row processing
491
- * Handles large files by processing rows in batches
490
+ * ✅ UPDATED: Stream Excel file with optional headerRow override
492
491
  */
493
492
  async streamFileMultiSheet(file, options = {}) {
494
- const { maxSheetSize = 100000, sheetChunkSize = 10000, onProgress } = options;
493
+ const { maxSheetSize = 100000, sheetChunkSize = 10000, onProgress, headerRow } = options;
495
494
  let XLSX;
496
495
  try {
497
496
  XLSX = await import('xlsx');
@@ -506,63 +505,29 @@ export class TabularDriver {
506
505
  onProgress(file.size * 0.3, file.size);
507
506
  const workbook = XLSX.read(buffer, {
508
507
  type: 'array',
509
- sheetRows: maxSheetSize, // ✅ Limit rows read per sheet
510
- dense: false // ✅ Use sparse format for memory efficiency
508
+ sheetRows: maxSheetSize,
509
+ dense: false
511
510
  });
512
511
  const sheets = {};
513
512
  const totalSheets = workbook.SheetNames.length;
514
513
  let processedSheets = 0;
515
514
  for (const sheetName of workbook.SheetNames) {
516
515
  const worksheet = workbook.Sheets[sheetName];
517
- // ✅ Get sheet range to determine size
518
516
  const range = XLSX.utils.decode_range(worksheet['!ref'] || 'A1');
519
517
  const totalRows = range.e.r - range.s.r + 1;
520
518
  if (totalRows === 0) {
521
519
  processedSheets++;
522
520
  continue;
523
521
  }
524
- // ✅ Process in chunks if sheet is large
525
- if (totalRows > sheetChunkSize) {
526
- const rows = [];
527
- let headers = [];
528
- for (let startRow = 0; startRow < totalRows; startRow += sheetChunkSize) {
529
- const endRow = Math.min(startRow + sheetChunkSize, totalRows);
530
- // ✅ Read chunk with limited row range
531
- const chunkData = XLSX.utils.sheet_to_json(worksheet, {
532
- range: startRow,
533
- header: startRow === 0 ? undefined : headers,
534
- defval: null,
535
- raw: false, // ✅ Convert dates/numbers to strings to reduce memory
536
- blankrows: false
537
- });
538
- if (startRow === 0 && chunkData.length > 0) {
539
- headers = Object.keys(chunkData[0]);
540
- }
541
- rows.push(...chunkData);
542
- if (onProgress) {
543
- const progress = 0.3 + (0.6 * (processedSheets + (startRow / totalRows)) / totalSheets);
544
- onProgress(file.size * progress, file.size);
545
- }
546
- // ✅ Stop if we hit max rows
547
- if (rows.length >= maxSheetSize) {
548
- console.warn(`⚠️ Sheet "${sheetName}" truncated to ${maxSheetSize} rows`);
549
- break;
550
- }
551
- }
552
- if (rows.length > 0) {
553
- sheets[sheetName] = new DataFrame(rows);
554
- }
555
- }
556
- else {
557
- // ✅ Small sheet: process normally
558
- const jsonData = XLSX.utils.sheet_to_json(worksheet, {
559
- defval: null,
560
- raw: false,
561
- blankrows: false
562
- });
563
- if (jsonData.length > 0) {
564
- sheets[sheetName] = new DataFrame(jsonData);
565
- }
522
+ // ✅ NEW: Support headerRow option
523
+ const jsonData = XLSX.utils.sheet_to_json(worksheet, {
524
+ range: headerRow !== undefined ? headerRow : undefined, // ✅ Start from specified row
525
+ defval: null,
526
+ raw: false,
527
+ blankrows: false
528
+ });
529
+ if (jsonData.length > 0) {
530
+ sheets[sheetName] = new DataFrame(jsonData);
566
531
  }
567
532
  processedSheets++;
568
533
  if (onProgress) {
@@ -607,11 +607,10 @@ export class TabularDriver {
607
607
  }
608
608
 
609
609
  /**
610
- * ✅ OPTIMIZED: Stream Excel file with chunked row processing
611
- * Handles large files by processing rows in batches
610
+ * ✅ UPDATED: Stream Excel file with optional headerRow override
612
611
  */
613
612
  async streamFileMultiSheet(file: File, options: ParseOptions = {}): Promise<Record<string, DataFrame>> {
614
- const { maxSheetSize = 100000, sheetChunkSize = 10000, onProgress } = options;
613
+ const { maxSheetSize = 100000, sheetChunkSize = 10000, onProgress, headerRow } = options;
615
614
 
616
615
  let XLSX: any;
617
616
  try {
@@ -628,8 +627,8 @@ export class TabularDriver {
628
627
 
629
628
  const workbook = XLSX.read(buffer, {
630
629
  type: 'array',
631
- sheetRows: maxSheetSize, // ✅ Limit rows read per sheet
632
- dense: false // ✅ Use sparse format for memory efficiency
630
+ sheetRows: maxSheetSize,
631
+ dense: false
633
632
  });
634
633
 
635
634
  const sheets: Record<string, DataFrame> = {};
@@ -639,7 +638,6 @@ export class TabularDriver {
639
638
  for (const sheetName of workbook.SheetNames) {
640
639
  const worksheet = workbook.Sheets[sheetName];
641
640
 
642
- // ✅ Get sheet range to determine size
643
641
  const range = XLSX.utils.decode_range(worksheet['!ref'] || 'A1');
644
642
  const totalRows = range.e.r - range.s.r + 1;
645
643
 
@@ -648,55 +646,16 @@ export class TabularDriver {
648
646
  continue;
649
647
  }
650
648
 
651
- // ✅ Process in chunks if sheet is large
652
- if (totalRows > sheetChunkSize) {
653
- const rows: Record<string, any>[] = [];
654
- let headers: string[] = [];
655
-
656
- for (let startRow = 0; startRow < totalRows; startRow += sheetChunkSize) {
657
- const endRow = Math.min(startRow + sheetChunkSize, totalRows);
658
-
659
- // ✅ Read chunk with limited row range
660
- const chunkData: Record<string, any>[] = XLSX.utils.sheet_to_json(worksheet, {
661
- range: startRow,
662
- header: startRow === 0 ? undefined : headers,
663
- defval: null,
664
- raw: false, // ✅ Convert dates/numbers to strings to reduce memory
665
- blankrows: false
666
- });
667
-
668
- if (startRow === 0 && chunkData.length > 0) {
669
- headers = Object.keys(chunkData[0]);
670
- }
671
-
672
- rows.push(...chunkData);
673
-
674
- if (onProgress) {
675
- const progress = 0.3 + (0.6 * (processedSheets + (startRow / totalRows)) / totalSheets);
676
- onProgress(file.size * progress, file.size);
677
- }
678
-
679
- // ✅ Stop if we hit max rows
680
- if (rows.length >= maxSheetSize) {
681
- console.warn(`⚠️ Sheet "${sheetName}" truncated to ${maxSheetSize} rows`);
682
- break;
683
- }
684
- }
685
-
686
- if (rows.length > 0) {
687
- sheets[sheetName] = new DataFrame(rows);
688
- }
689
- } else {
690
- // ✅ Small sheet: process normally
691
- const jsonData: Record<string, any>[] = XLSX.utils.sheet_to_json(worksheet, {
692
- defval: null,
693
- raw: false,
694
- blankrows: false
695
- });
649
+ // ✅ NEW: Support headerRow option
650
+ const jsonData: Record<string, any>[] = XLSX.utils.sheet_to_json(worksheet, {
651
+ range: headerRow !== undefined ? headerRow : undefined, // Start from specified row
652
+ defval: null,
653
+ raw: false,
654
+ blankrows: false
655
+ });
696
656
 
697
- if (jsonData.length > 0) {
698
- sheets[sheetName] = new DataFrame(jsonData);
699
- }
657
+ if (jsonData.length > 0) {
658
+ sheets[sheetName] = new DataFrame(jsonData);
700
659
  }
701
660
 
702
661
  processedSheets++;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "juxscript",
3
- "version": "1.1.183",
3
+ "version": "1.1.185",
4
4
  "type": "module",
5
5
  "description": "A JavaScript UX authorship platform",
6
6
  "main": "index.js",