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.
- package/lib/components/dataframe.d.ts +11 -0
- package/lib/components/dataframe.d.ts.map +1 -1
- package/lib/components/dataframe.js +231 -6
- package/lib/components/dataframe.ts +261 -6
- package/lib/storage/TabularDriver.d.ts +1 -2
- package/lib/storage/TabularDriver.d.ts.map +1 -1
- package/lib/storage/TabularDriver.js +13 -48
- package/lib/storage/TabularDriver.ts +13 -54
- package/package.json +1 -1
|
@@ -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":"AAAA,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AACnE,OAAO,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AACpD,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAC5D,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAOnC,MAAM,WAAW,gBAAgB;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,KAAK,cAAc,GAAG,SAAS,GAAG;IAC9B,MAAM,EAAE,OAAO,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,qBAAa,kBAAmB,SAAQ,aAAa,CAAC,cAAc,CAAC;IACjE,OAAO,CAAC,GAAG,CAA0B;IACrC,OAAO,CAAC,OAAO,CAAgB;IAC/B,OAAO,CAAC,MAAM,CAAsB;IACpC,OAAO,CAAC,KAAK,CAAqB;IAClC,OAAO,CAAC,OAAO,CAAqC;IACpD,OAAO,CAAC,aAAa,CAOnB;IACF,OAAO,CAAC,UAAU,CAA2B;IAC7C,OAAO,CAAC,WAAW,CAAuB;IAC1C,OAAO,CAAC,cAAc,CAAsC;IAC5D,OAAO,CAAC,aAAa,CAAgE;IACrF,OAAO,CAAC,WAAW,CAAiB;IACpC,OAAO,CAAC,KAAK,CAAc;IAC3B,OAAO,CAAC,aAAa,CAAkB;IACvC,OAAO,CAAC,eAAe,CAAiB;IACxC,OAAO,CAAC,YAAY,CAAc;IAClC,OAAO,CAAC,mBAAmB,CAAiB;IAC5C,OAAO,CAAC,YAAY,
|
|
1
|
+
{"version":3,"file":"dataframe.d.ts","sourceRoot":"","sources":["dataframe.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AACnE,OAAO,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AACpD,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAC5D,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAOnC,MAAM,WAAW,gBAAgB;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,KAAK,cAAc,GAAG,SAAS,GAAG;IAC9B,MAAM,EAAE,OAAO,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,qBAAa,kBAAmB,SAAQ,aAAa,CAAC,cAAc,CAAC;IACjE,OAAO,CAAC,GAAG,CAA0B;IACrC,OAAO,CAAC,OAAO,CAAgB;IAC/B,OAAO,CAAC,MAAM,CAAsB;IACpC,OAAO,CAAC,KAAK,CAAqB;IAClC,OAAO,CAAC,OAAO,CAAqC;IACpD,OAAO,CAAC,aAAa,CAOnB;IACF,OAAO,CAAC,UAAU,CAA2B;IAC7C,OAAO,CAAC,WAAW,CAAuB;IAC1C,OAAO,CAAC,cAAc,CAAsC;IAC5D,OAAO,CAAC,aAAa,CAAgE;IACrF,OAAO,CAAC,WAAW,CAAiB;IACpC,OAAO,CAAC,KAAK,CAAc;IAC3B,OAAO,CAAC,aAAa,CAAkB;IACvC,OAAO,CAAC,eAAe,CAAiB;IACxC,OAAO,CAAC,YAAY,CAAc;IAClC,OAAO,CAAC,mBAAmB,CAAiB;IAC5C,OAAO,CAAC,YAAY,CAAiE;gBAEzE,EAAE,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAqB;IAmCtD,SAAS,CAAC,gBAAgB,IAAI,SAAS,MAAM,EAAE;IAC/C,SAAS,CAAC,iBAAiB,IAAI,SAAS,MAAM,EAAE;IAMhD,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAwB9B,UAAU,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI;IAmEpC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,GAAG,IAAI;IAiBnE,UAAU,CAAC,KAAK,GAAE,MAAsB,EAAE,MAAM,GAAE,MAAoC,EAAE,IAAI,GAAE,MAAiB,GAAG,IAAI;IAStH,UAAU,CAAC,CAAC,EAAE,OAAO,GAAG,IAAI;IAC5B,UAAU,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI;IAM3B,KAAK,CAAC,EAAE,EAAE,CAAC,EAAE,EAAE,SAAS,KAAK,SAAS,GAAG,IAAI;IAQ7C,MAAM,CAAC,SAAS,EAAE,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,GAAG,IAAI;IAI7E,MAAM,CAAC,GAAG,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI;IAI/B,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,OAAO,GAAG,IAAI;IAI7C,IAAI,CAAC,CAAC,GAAE,MAAU,GAAG,IAAI;IAIzB,IAAI,CAAC,CAAC,GAAE,MAAU,GAAG,IAAI;IAIzB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,KAAK,EAAE,MAAM,KAAK,GAAG,GAAG,IAAI;IAIpF,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE,IAAI,GAAG,IAAI,GAAG,GAAG,GAAG,GAAG,GAAG,IAAI,GAAG,IAAI,GAAG,UAAU,GAAG,YAAY,GAAG,UAAU,EAAE,KAAK,EAAE,GAAG,GAAG,IAAI;IAQxH,IAAI,EAAE,IAAI,SAAS,GAAG,IAAI,CAAqB;IAC/C,IAAI,MAAM,IAAI,aAAa,CAAyB;IACpD,IAAI,KAAK,IAAI,KAAK,GAAG,IAAI,CAAwB;IACjD,QAAQ,IAAI,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI;IACtC,KAAK,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM;IACjC,MAAM,IAAI,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE;IAC/B,IAAI,KAAK,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,CAAsC;IACnE,IAAI,OAAO,IAAI,MAAM,EAAE,CAAoC;IAErD,IAAI,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAUhD,OAAO,CAAC,CAAC,EAAE,OAAO,GAAG,IAAI;IACzB,SAAS,CAAC,CAAC,EAAE,OAAO,GAAG,IAAI;IAC3B,QAAQ,CAAC,CAAC,EAAE,OAAO,GAAG,IAAI;IAC1B,UAAU,CAAC,CAAC,EAAE,OAAO,GAAG,IAAI;IAC5B,SAAS,CAAC,CAAC,EAAE,OAAO,GAAG,IAAI;IAC3B,WAAW,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI;IAE5B;;OAEG;IACH,YAAY,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI;IAK7B;;OAEG;IACH,cAAc,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI;IAK/B;;OAEG;IACH,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;IAS7B;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAkIzB,OAAO,CAAC,aAAa;IAuBrB,OAAO,CAAC,aAAa;IAmCrB,OAAO,CAAC,YAAY;IAwCpB,OAAO,CAAC,gBAAgB;IA+CxB;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAczB;;OAEG;YACW,sBAAsB;IAuGpC;;OAEG;IACH,OAAO,CAAC,oBAAoB;IAgI5B,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,GAAG,IAAI;IAMtC,MAAM,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,WAAW,GAAG,aAAa,CAAC,GAAG,CAAC,GAAG,IAAI;CA4GrE;AAED,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAqB,GAAG,kBAAkB,CAExF"}
|
|
@@ -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; // ✅
|
|
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
|
|
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
|
-
|
|
486
|
-
|
|
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; // ✅
|
|
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
|
|
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
|
-
|
|
601
|
-
|
|
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
|
-
* ✅
|
|
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":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAE3C,MAAM,WAAW,WAAW;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,IAAI,EAAE,GAAG,EAAE,EAAE,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;CAClC;AAED,MAAM,WAAW,YAAY;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IAC5D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mBAAmB,CAAC,EAAE,OAAO,CAAC;CACjC;AAED,qBAAa,aAAa;IACtB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,GAAG,CAA4B;gBAE3B,MAAM,GAAE,MAAsB,EAAE,SAAS,GAAE,MAAiB;IAKlE,IAAI,IAAI,OAAO,CAAC,WAAW,CAAC;IA4BlC;;;OAGG;IACH,OAAO,CAAC,gBAAgB;IAgCxB;;;OAGG;IACH,OAAO,CAAC,gBAAgB;IAwBxB;;OAEG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,YAAiB,GAAG,SAAS;IA6D7D;;;OAGG;IACG,UAAU,CAAC,IAAI,EAAE,IAAI,EAAE,OAAO,GAAE,YAAiB,GAAG,OAAO,CAAC,SAAS,CAAC;IAoG5E;;OAEG;YACW,UAAU;IAuExB;;OAEG;IACG,aAAa,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAiBlD;;OAEG;IACG,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,SAAS,EAAE,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC;IAuBzF;;OAEG;IACG,IAAI,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC;IAwBjD;;OAEG;IACG,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC;IA4BzD;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,KAAK,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,EAAE,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAqBlH;;OAEG;IACG,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAYjC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAgB5B;;OAEG;IACG,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,YAAiB,GAAG,OAAO,CAAC,SAAS,CAAC;IA0ExE
|
|
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
|
-
* ✅
|
|
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,
|
|
510
|
-
dense: false
|
|
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
|
-
// ✅
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
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
|
-
* ✅
|
|
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,
|
|
632
|
-
dense: false
|
|
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
|
-
// ✅
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
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
|
-
|
|
698
|
-
|
|
699
|
-
}
|
|
657
|
+
if (jsonData.length > 0) {
|
|
658
|
+
sheets[sheetName] = new DataFrame(jsonData);
|
|
700
659
|
}
|
|
701
660
|
|
|
702
661
|
processedSheets++;
|