juxscript 1.1.168 → 1.1.170
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 +48 -13
- package/lib/components/dataframe.d.ts.map +1 -1
- package/lib/components/dataframe.js +104 -96
- package/lib/components/dataframe.ts +118 -106
- package/package.json +1 -1
|
@@ -12,8 +12,6 @@ export interface DataFrameOptions {
|
|
|
12
12
|
filterable?: boolean;
|
|
13
13
|
paginated?: boolean;
|
|
14
14
|
rowsPerPage?: number;
|
|
15
|
-
showStatus?: boolean;
|
|
16
|
-
icon?: string;
|
|
17
15
|
style?: string;
|
|
18
16
|
class?: string;
|
|
19
17
|
}
|
|
@@ -32,41 +30,78 @@ export declare class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
32
30
|
private _storageKey;
|
|
33
31
|
private _pendingSource;
|
|
34
32
|
private _inlineUpload;
|
|
35
|
-
private _showStatus;
|
|
36
|
-
private _icon;
|
|
37
33
|
constructor(id: string, options?: DataFrameOptions);
|
|
38
34
|
protected getTriggerEvents(): readonly string[];
|
|
39
35
|
protected getCallbackEvents(): readonly string[];
|
|
36
|
+
/**
|
|
37
|
+
* Load from IndexedDB by storage key
|
|
38
|
+
*/
|
|
40
39
|
fromStorage(key: string): this;
|
|
40
|
+
/**
|
|
41
|
+
* Load from a FileUpload component — auto-wires change event
|
|
42
|
+
*/
|
|
41
43
|
fromUpload(upload: FileUpload): this;
|
|
44
|
+
/**
|
|
45
|
+
* Load from raw data — array of objects or Record<string, any[]>
|
|
46
|
+
*/
|
|
42
47
|
fromData(data: Record<string, any>[] | Record<string, any[]>): this;
|
|
43
48
|
/**
|
|
44
|
-
* Add an inline file upload control.
|
|
45
|
-
*
|
|
49
|
+
* Add an inline file upload control above the table.
|
|
50
|
+
* Auto-wires parsing, storage, and display — zero callbacks needed.
|
|
51
|
+
*
|
|
52
|
+
* @param label - Button/label text (default: 'Upload File')
|
|
46
53
|
* @param accept - File types (default: '.csv,.tsv,.txt,.xlsx,.xls')
|
|
47
|
-
* @param icon - Upload icon name (default: 'upload'). Pass '' to hide icon.
|
|
48
54
|
*/
|
|
49
|
-
withUpload(label?: string, accept?: string
|
|
50
|
-
/**
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
statusIcon(v: string): this;
|
|
55
|
+
withUpload(label?: string, accept?: string): this;
|
|
56
|
+
/**
|
|
57
|
+
* Apply a transform to the current DataFrame and update the table
|
|
58
|
+
*/
|
|
54
59
|
apply(fn: (df: DataFrame) => DataFrame): this;
|
|
60
|
+
/**
|
|
61
|
+
* Filter rows
|
|
62
|
+
*/
|
|
55
63
|
filter(predicate: (row: Record<string, any>, index: number) => boolean): this;
|
|
64
|
+
/**
|
|
65
|
+
* Select columns
|
|
66
|
+
*/
|
|
56
67
|
select(...cols: string[]): this;
|
|
68
|
+
/**
|
|
69
|
+
* Sort by column
|
|
70
|
+
*/
|
|
57
71
|
sort(col: string, descending?: boolean): this;
|
|
72
|
+
/**
|
|
73
|
+
* Show first N rows
|
|
74
|
+
*/
|
|
58
75
|
head(n?: number): this;
|
|
76
|
+
/**
|
|
77
|
+
* Show last N rows
|
|
78
|
+
*/
|
|
59
79
|
tail(n?: number): this;
|
|
80
|
+
/**
|
|
81
|
+
* Add a computed column
|
|
82
|
+
*/
|
|
60
83
|
withColumn(name: string, fn: (row: Record<string, any>, index: number) => any): this;
|
|
84
|
+
/**
|
|
85
|
+
* Where clause
|
|
86
|
+
*/
|
|
61
87
|
where(col: string, op: '==' | '!=' | '>' | '<' | '>=' | '<=' | 'contains' | 'startsWith' | 'endsWith', value: any): this;
|
|
88
|
+
/** Get the underlying DataFrame */
|
|
62
89
|
get df(): DataFrame | null;
|
|
90
|
+
/** Get the underlying TabularDriver */
|
|
63
91
|
get driver(): TabularDriver;
|
|
92
|
+
/** Get the internal Table component */
|
|
64
93
|
get table(): Table | null;
|
|
94
|
+
/** Get describe() stats */
|
|
65
95
|
describe(): Record<string, any> | null;
|
|
96
|
+
/** Export to CSV string */
|
|
66
97
|
toCSV(delimiter?: string): string;
|
|
98
|
+
/** Export to row objects */
|
|
67
99
|
toRows(): Record<string, any>[];
|
|
100
|
+
/** Get shape */
|
|
68
101
|
get shape(): [number, number];
|
|
102
|
+
/** Get column names */
|
|
69
103
|
get columns(): string[];
|
|
104
|
+
/** Save current DataFrame to IndexedDB */
|
|
70
105
|
save(key?: string): Promise<string | null>;
|
|
71
106
|
striped(v: boolean): this;
|
|
72
107
|
hoverable(v: boolean): this;
|
|
@@ -77,8 +112,8 @@ export declare class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
77
112
|
private _updateStatus;
|
|
78
113
|
private _setDataFrame;
|
|
79
114
|
private _updateTable;
|
|
80
|
-
private _showFilterInput;
|
|
81
115
|
update(prop: string, value: any): void;
|
|
116
|
+
private _showFilterInput;
|
|
82
117
|
render(targetId?: string | HTMLElement | BaseComponent<any>): this;
|
|
83
118
|
}
|
|
84
119
|
export declare function dataframe(id: string, options?: DataFrameOptions): DataFrameComponent;
|
|
@@ -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;AAMnC,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,
|
|
1
|
+
{"version":3,"file":"dataframe.d.ts","sourceRoot":"","sources":["dataframe.ts"],"names":[],"mappings":"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"}
|
|
@@ -25,16 +25,12 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
25
25
|
this._storageKey = null;
|
|
26
26
|
this._pendingSource = null;
|
|
27
27
|
this._inlineUpload = null;
|
|
28
|
-
this._showStatus = true;
|
|
29
|
-
this._icon = '';
|
|
30
28
|
this._driver = new TabularDriver(options.dbName ?? 'jux-dataframes', options.storeName ?? 'frames');
|
|
31
|
-
this._showStatus = options.showStatus ?? true;
|
|
32
|
-
this._icon = options.icon ?? '';
|
|
33
29
|
this._tableOptions = {
|
|
34
30
|
striped: options.striped ?? true,
|
|
35
31
|
hoverable: options.hoverable ?? true,
|
|
36
32
|
sortable: options.sortable ?? true,
|
|
37
|
-
filterable: options.filterable ??
|
|
33
|
+
filterable: options.filterable ?? true,
|
|
38
34
|
paginated: options.paginated ?? true,
|
|
39
35
|
rowsPerPage: options.rowsPerPage ?? 25
|
|
40
36
|
};
|
|
@@ -44,17 +40,18 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
44
40
|
/* ═══════════════════════════════════════════════════
|
|
45
41
|
* DATA SOURCES
|
|
46
42
|
* ═══════════════════════════════════════════════════ */
|
|
43
|
+
/**
|
|
44
|
+
* Load from IndexedDB by storage key
|
|
45
|
+
*/
|
|
47
46
|
fromStorage(key) {
|
|
48
47
|
this._storageKey = key;
|
|
49
|
-
|
|
48
|
+
this._pendingSource = async () => {
|
|
50
49
|
this.state.loading = true;
|
|
51
|
-
this._updateStatus('⏳ Loading...', 'loading');
|
|
52
50
|
try {
|
|
53
|
-
|
|
51
|
+
let df = await this._driver.loadByName(key);
|
|
54
52
|
if (!df) {
|
|
55
53
|
this._triggerCallback('error', 'No table found with key: ' + key, null, this);
|
|
56
54
|
this.state.loading = false;
|
|
57
|
-
this._updateStatus('No table found: ' + key, 'empty');
|
|
58
55
|
return;
|
|
59
56
|
}
|
|
60
57
|
this._setDataFrame(df, 'storage: ' + key);
|
|
@@ -62,44 +59,42 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
62
59
|
catch (err) {
|
|
63
60
|
this._triggerCallback('error', err.message, null, this);
|
|
64
61
|
this.state.loading = false;
|
|
65
|
-
this._updateStatus('❌ ' + err.message, 'error');
|
|
66
62
|
}
|
|
67
63
|
};
|
|
68
|
-
if (this._table) {
|
|
69
|
-
loadFn();
|
|
70
|
-
}
|
|
71
|
-
else {
|
|
72
|
-
this._pendingSource = loadFn;
|
|
73
|
-
}
|
|
74
64
|
return this;
|
|
75
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* Load from a FileUpload component — auto-wires change event
|
|
68
|
+
*/
|
|
76
69
|
fromUpload(upload) {
|
|
77
70
|
this._uploadRef = upload;
|
|
78
71
|
this._pendingSource = async () => {
|
|
72
|
+
// Wire upload's change event to parse incoming files
|
|
79
73
|
upload.bind('change', async (files) => {
|
|
80
74
|
if (!files || files.length === 0)
|
|
81
75
|
return;
|
|
82
76
|
const file = files[0];
|
|
83
77
|
this.state.loading = true;
|
|
84
|
-
this._updateStatus('⏳ Parsing ' + file.name + '...', 'loading');
|
|
85
78
|
try {
|
|
86
79
|
const df = await this._driver.streamFile(file);
|
|
80
|
+
// Auto-persist to IndexedDB
|
|
87
81
|
await this._driver.store(file.name, df, { source: file.name });
|
|
88
|
-
this._setDataFrame(df, file.name);
|
|
82
|
+
this._setDataFrame(df, 'upload: ' + file.name);
|
|
89
83
|
}
|
|
90
84
|
catch (err) {
|
|
91
85
|
this._triggerCallback('error', err.message, null, this);
|
|
92
86
|
this.state.loading = false;
|
|
93
|
-
this._updateStatus('❌ ' + err.message, 'error');
|
|
94
87
|
}
|
|
95
88
|
});
|
|
96
89
|
};
|
|
97
90
|
return this;
|
|
98
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* Load from raw data — array of objects or Record<string, any[]>
|
|
94
|
+
*/
|
|
99
95
|
fromData(data) {
|
|
100
|
-
|
|
96
|
+
this._pendingSource = async () => {
|
|
101
97
|
this.state.loading = true;
|
|
102
|
-
this._updateStatus('⏳ Loading data...', 'loading');
|
|
103
98
|
try {
|
|
104
99
|
const df = new DataFrame(data);
|
|
105
100
|
this._setDataFrame(df, 'inline data');
|
|
@@ -107,37 +102,27 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
107
102
|
catch (err) {
|
|
108
103
|
this._triggerCallback('error', err.message, null, this);
|
|
109
104
|
this.state.loading = false;
|
|
110
|
-
this._updateStatus('❌ ' + err.message, 'error');
|
|
111
105
|
}
|
|
112
106
|
};
|
|
113
|
-
if (this._table) {
|
|
114
|
-
loadFn();
|
|
115
|
-
}
|
|
116
|
-
else {
|
|
117
|
-
this._pendingSource = loadFn;
|
|
118
|
-
}
|
|
119
107
|
return this;
|
|
120
108
|
}
|
|
121
109
|
/**
|
|
122
|
-
* Add an inline file upload control.
|
|
123
|
-
*
|
|
110
|
+
* Add an inline file upload control above the table.
|
|
111
|
+
* Auto-wires parsing, storage, and display — zero callbacks needed.
|
|
112
|
+
*
|
|
113
|
+
* @param label - Button/label text (default: 'Upload File')
|
|
124
114
|
* @param accept - File types (default: '.csv,.tsv,.txt,.xlsx,.xls')
|
|
125
|
-
* @param icon - Upload icon name (default: 'upload'). Pass '' to hide icon.
|
|
126
115
|
*/
|
|
127
|
-
withUpload(label = 'Upload File', accept = '.csv,.tsv,.txt,.xlsx,.xls'
|
|
128
|
-
this._inlineUpload = { label, accept
|
|
116
|
+
withUpload(label = 'Upload File', accept = '.csv,.tsv,.txt,.xlsx,.xls') {
|
|
117
|
+
this._inlineUpload = { label, accept };
|
|
129
118
|
return this;
|
|
130
119
|
}
|
|
131
120
|
/* ═══════════════════════════════════════════════════
|
|
132
|
-
*
|
|
133
|
-
* ═══════════════════════════════════════════════════ */
|
|
134
|
-
/** Show/hide the status bar */
|
|
135
|
-
showStatus(v) { this._showStatus = v; return this; }
|
|
136
|
-
/** Set a custom icon for the status bar */
|
|
137
|
-
statusIcon(v) { this._icon = v; return this; }
|
|
138
|
-
/* ═══════════════════════════════════════════════════
|
|
139
|
-
* TRANSFORM API
|
|
121
|
+
* TRANSFORM API (returns new DataFrameComponent view)
|
|
140
122
|
* ═══════════════════════════════════════════════════ */
|
|
123
|
+
/**
|
|
124
|
+
* Apply a transform to the current DataFrame and update the table
|
|
125
|
+
*/
|
|
141
126
|
apply(fn) {
|
|
142
127
|
if (!this._df)
|
|
143
128
|
return this;
|
|
@@ -146,38 +131,78 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
146
131
|
this._triggerCallback('transform', result, null, this);
|
|
147
132
|
return this;
|
|
148
133
|
}
|
|
134
|
+
/**
|
|
135
|
+
* Filter rows
|
|
136
|
+
*/
|
|
149
137
|
filter(predicate) {
|
|
150
138
|
return this.apply(df => df.filter(predicate));
|
|
151
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* Select columns
|
|
142
|
+
*/
|
|
152
143
|
select(...cols) {
|
|
153
144
|
return this.apply(df => df.select(...cols));
|
|
154
145
|
}
|
|
146
|
+
/**
|
|
147
|
+
* Sort by column
|
|
148
|
+
*/
|
|
155
149
|
sort(col, descending) {
|
|
156
150
|
return this.apply(df => df.sort(col, descending));
|
|
157
151
|
}
|
|
152
|
+
/**
|
|
153
|
+
* Show first N rows
|
|
154
|
+
*/
|
|
158
155
|
head(n = 5) {
|
|
159
156
|
return this.apply(df => df.head(n));
|
|
160
157
|
}
|
|
158
|
+
/**
|
|
159
|
+
* Show last N rows
|
|
160
|
+
*/
|
|
161
161
|
tail(n = 5) {
|
|
162
162
|
return this.apply(df => df.tail(n));
|
|
163
163
|
}
|
|
164
|
+
/**
|
|
165
|
+
* Add a computed column
|
|
166
|
+
*/
|
|
164
167
|
withColumn(name, fn) {
|
|
165
168
|
return this.apply(df => df.withColumn(name, fn));
|
|
166
169
|
}
|
|
170
|
+
/**
|
|
171
|
+
* Where clause
|
|
172
|
+
*/
|
|
167
173
|
where(col, op, value) {
|
|
168
174
|
return this.apply(df => df.where(col, op, value));
|
|
169
175
|
}
|
|
170
176
|
/* ═══════════════════════════════════════════════════
|
|
171
177
|
* ACCESSORS
|
|
172
178
|
* ═══════════════════════════════════════════════════ */
|
|
179
|
+
/** Get the underlying DataFrame */
|
|
173
180
|
get df() { return this._df; }
|
|
181
|
+
/** Get the underlying TabularDriver */
|
|
174
182
|
get driver() { return this._driver; }
|
|
183
|
+
/** Get the internal Table component */
|
|
175
184
|
get table() { return this._table; }
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
185
|
+
/** Get describe() stats */
|
|
186
|
+
describe() {
|
|
187
|
+
return this._df?.describe() ?? null;
|
|
188
|
+
}
|
|
189
|
+
/** Export to CSV string */
|
|
190
|
+
toCSV(delimiter) {
|
|
191
|
+
return this._df?.toCSV(delimiter) ?? '';
|
|
192
|
+
}
|
|
193
|
+
/** Export to row objects */
|
|
194
|
+
toRows() {
|
|
195
|
+
return this._df?.toRows() ?? [];
|
|
196
|
+
}
|
|
197
|
+
/** Get shape */
|
|
198
|
+
get shape() {
|
|
199
|
+
return this._df?.shape ?? [0, 0];
|
|
200
|
+
}
|
|
201
|
+
/** Get column names */
|
|
202
|
+
get columns() {
|
|
203
|
+
return this._df?.columns ?? [];
|
|
204
|
+
}
|
|
205
|
+
/** Save current DataFrame to IndexedDB */
|
|
181
206
|
async save(key) {
|
|
182
207
|
if (!this._df)
|
|
183
208
|
return null;
|
|
@@ -196,26 +221,10 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
196
221
|
/* ═══════════════════════════════════════════════════
|
|
197
222
|
* INTERNAL
|
|
198
223
|
* ═══════════════════════════════════════════════════ */
|
|
199
|
-
_updateStatus(text
|
|
224
|
+
_updateStatus(text) {
|
|
200
225
|
const el = document.getElementById(`${this._id}-status`);
|
|
201
|
-
if (
|
|
202
|
-
|
|
203
|
-
el.className = 'jux-dataframe-status';
|
|
204
|
-
if (type)
|
|
205
|
-
el.classList.add(`jux-dataframe-status-${type}`);
|
|
206
|
-
// Clear and rebuild
|
|
207
|
-
el.innerHTML = '';
|
|
208
|
-
if (this._icon && type === 'success') {
|
|
209
|
-
const iconEl = renderIcon(this._icon);
|
|
210
|
-
iconEl.style.width = '16px';
|
|
211
|
-
iconEl.style.height = '16px';
|
|
212
|
-
iconEl.style.marginRight = '6px';
|
|
213
|
-
iconEl.style.verticalAlign = 'middle';
|
|
214
|
-
el.appendChild(iconEl);
|
|
215
|
-
}
|
|
216
|
-
const span = document.createElement('span');
|
|
217
|
-
span.textContent = text;
|
|
218
|
-
el.appendChild(span);
|
|
226
|
+
if (el)
|
|
227
|
+
el.textContent = text;
|
|
219
228
|
}
|
|
220
229
|
_setDataFrame(df, sourceName) {
|
|
221
230
|
this._df = df;
|
|
@@ -229,18 +238,22 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
229
238
|
this._df = df.select(...cleanCols);
|
|
230
239
|
}
|
|
231
240
|
this._updateTable();
|
|
232
|
-
this._updateStatus(
|
|
233
|
-
|
|
241
|
+
this._updateStatus(`✅ ${sourceName} — ${this._df.height} rows × ${this._df.width} cols`);
|
|
242
|
+
this._triggerCallback('load', this._df, null, this);
|
|
243
|
+
// ✅ Enable filter now that data exists (render it into DOM)
|
|
234
244
|
if (this._tableOptions.filterable) {
|
|
235
245
|
this._showFilterInput();
|
|
236
246
|
}
|
|
237
|
-
this._triggerCallback('load', this._df, null, this);
|
|
238
247
|
}
|
|
239
248
|
_updateTable() {
|
|
240
249
|
if (!this._table || !this._df)
|
|
241
250
|
return;
|
|
242
251
|
this._table.columns(this._df.columns).rows(this._df.toRows());
|
|
243
252
|
}
|
|
253
|
+
update(prop, value) { }
|
|
254
|
+
/* ═══════════════════════════════════════════════════
|
|
255
|
+
* RENDER
|
|
256
|
+
* ═══════════════════════════════════════════════════ */
|
|
244
257
|
_showFilterInput() {
|
|
245
258
|
const wrapper = document.getElementById(this._id);
|
|
246
259
|
if (!wrapper)
|
|
@@ -262,12 +275,14 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
262
275
|
iconWrap.appendChild(iconEl);
|
|
263
276
|
filterContainer.appendChild(iconWrap);
|
|
264
277
|
filterContainer.appendChild(input);
|
|
265
|
-
//
|
|
266
|
-
const
|
|
267
|
-
if (
|
|
268
|
-
|
|
278
|
+
// ✅ FIX: Find the table element (we know it exists because render() created it)
|
|
279
|
+
const tableElement = wrapper.querySelector('.jux-table-wrapper table');
|
|
280
|
+
if (tableElement && tableElement.parentElement) {
|
|
281
|
+
// Insert filter BEFORE the table element
|
|
282
|
+
tableElement.parentElement.insertBefore(filterContainer, tableElement);
|
|
269
283
|
}
|
|
270
284
|
else {
|
|
285
|
+
// Fallback: append to wrapper (shouldn't happen, but safe)
|
|
271
286
|
wrapper.appendChild(filterContainer);
|
|
272
287
|
}
|
|
273
288
|
input.addEventListener('input', () => {
|
|
@@ -284,10 +299,6 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
284
299
|
this._table?.rows(filtered.toRows());
|
|
285
300
|
});
|
|
286
301
|
}
|
|
287
|
-
update(prop, value) { }
|
|
288
|
-
/* ═══════════════════════════════════════════════════
|
|
289
|
-
* RENDER
|
|
290
|
-
* ═══════════════════════════════════════════════════ */
|
|
291
302
|
render(targetId) {
|
|
292
303
|
const container = this._setupContainer(targetId);
|
|
293
304
|
const { style, class: className } = this.state;
|
|
@@ -298,16 +309,13 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
298
309
|
wrapper.className += ` ${className}`;
|
|
299
310
|
if (style)
|
|
300
311
|
wrapper.setAttribute('style', style);
|
|
301
|
-
// Inline upload
|
|
312
|
+
// Inline upload (if withUpload was called)
|
|
302
313
|
if (this._inlineUpload) {
|
|
303
|
-
const
|
|
314
|
+
const upload = new FileUpload(`${this._id}-upload`, {
|
|
304
315
|
label: this._inlineUpload.label,
|
|
305
|
-
accept: this._inlineUpload.accept
|
|
306
|
-
};
|
|
307
|
-
|
|
308
|
-
uploadOpts.icon = this._inlineUpload.icon;
|
|
309
|
-
}
|
|
310
|
-
const upload = new FileUpload(`${this._id}-upload`, uploadOpts);
|
|
316
|
+
accept: this._inlineUpload.accept
|
|
317
|
+
});
|
|
318
|
+
// Wire it as a fromUpload source
|
|
311
319
|
this._uploadRef = upload;
|
|
312
320
|
this._pendingSource = async () => {
|
|
313
321
|
upload.bind('change', async (files) => {
|
|
@@ -315,7 +323,7 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
315
323
|
return;
|
|
316
324
|
const file = files[0];
|
|
317
325
|
this.state.loading = true;
|
|
318
|
-
this._updateStatus('⏳ Parsing ' + file.name + '...'
|
|
326
|
+
this._updateStatus('⏳ Parsing ' + file.name + '...');
|
|
319
327
|
try {
|
|
320
328
|
const df = await this._driver.streamFile(file);
|
|
321
329
|
await this._driver.store(file.name, df, { source: file.name });
|
|
@@ -324,10 +332,11 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
324
332
|
catch (err) {
|
|
325
333
|
this._triggerCallback('error', err.message, null, this);
|
|
326
334
|
this.state.loading = false;
|
|
327
|
-
this._updateStatus('❌ ' + err.message
|
|
335
|
+
this._updateStatus('❌ ' + err.message);
|
|
328
336
|
}
|
|
329
337
|
});
|
|
330
338
|
};
|
|
339
|
+
// Render upload into wrapper before status/table
|
|
331
340
|
const uploadContainer = document.createElement('div');
|
|
332
341
|
uploadContainer.className = 'jux-dataframe-upload';
|
|
333
342
|
uploadContainer.id = `${this._id}-upload-container`;
|
|
@@ -338,15 +347,14 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
338
347
|
else {
|
|
339
348
|
container.appendChild(wrapper);
|
|
340
349
|
}
|
|
341
|
-
// Status bar
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
// Table — filterable is false initially; we enable it after data loads
|
|
350
|
+
// Status bar
|
|
351
|
+
const statusBar = document.createElement('div');
|
|
352
|
+
statusBar.className = 'jux-dataframe-status';
|
|
353
|
+
statusBar.id = `${this._id}-status`;
|
|
354
|
+
statusBar.style.cssText = 'font-size:13px;color:#888;margin-bottom:8px;';
|
|
355
|
+
statusBar.textContent = 'No data loaded.';
|
|
356
|
+
wrapper.appendChild(statusBar);
|
|
357
|
+
// ✅ Create internal table — this establishes the DOM structure
|
|
350
358
|
const tbl = new Table(`${this._id}-table`, {
|
|
351
359
|
striped: this._tableOptions.striped,
|
|
352
360
|
hoverable: this._tableOptions.hoverable,
|
|
@@ -355,7 +363,7 @@ export class DataFrameComponent extends BaseComponent {
|
|
|
355
363
|
paginated: this._tableOptions.paginated,
|
|
356
364
|
rowsPerPage: this._tableOptions.rowsPerPage
|
|
357
365
|
});
|
|
358
|
-
tbl.render(wrapper);
|
|
366
|
+
tbl.render(wrapper); // ✅ Table now exists in wrapper
|
|
359
367
|
this._table = tbl;
|
|
360
368
|
// Execute pending data source
|
|
361
369
|
if (this._pendingSource) {
|
|
@@ -17,8 +17,6 @@ export interface DataFrameOptions {
|
|
|
17
17
|
filterable?: boolean;
|
|
18
18
|
paginated?: boolean;
|
|
19
19
|
rowsPerPage?: number;
|
|
20
|
-
showStatus?: boolean;
|
|
21
|
-
icon?: string;
|
|
22
20
|
style?: string;
|
|
23
21
|
class?: string;
|
|
24
22
|
}
|
|
@@ -45,9 +43,7 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
45
43
|
private _uploadRef: FileUpload | null = null;
|
|
46
44
|
private _storageKey: string | null = null;
|
|
47
45
|
private _pendingSource: (() => Promise<void>) | null = null;
|
|
48
|
-
private _inlineUpload: { label: string; accept: string
|
|
49
|
-
private _showStatus: boolean = true;
|
|
50
|
-
private _icon: string = '';
|
|
46
|
+
private _inlineUpload: { label: string; accept: string } | null = null;
|
|
51
47
|
|
|
52
48
|
constructor(id: string, options: DataFrameOptions = {}) {
|
|
53
49
|
super(id, {
|
|
@@ -67,14 +63,11 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
67
63
|
options.storeName ?? 'frames'
|
|
68
64
|
);
|
|
69
65
|
|
|
70
|
-
this._showStatus = options.showStatus ?? true;
|
|
71
|
-
this._icon = options.icon ?? '';
|
|
72
|
-
|
|
73
66
|
this._tableOptions = {
|
|
74
67
|
striped: options.striped ?? true,
|
|
75
68
|
hoverable: options.hoverable ?? true,
|
|
76
69
|
sortable: options.sortable ?? true,
|
|
77
|
-
filterable: options.filterable ??
|
|
70
|
+
filterable: options.filterable ?? true,
|
|
78
71
|
paginated: options.paginated ?? true,
|
|
79
72
|
rowsPerPage: options.rowsPerPage ?? 25
|
|
80
73
|
};
|
|
@@ -87,98 +80,91 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
87
80
|
* DATA SOURCES
|
|
88
81
|
* ═══════════════════════════════════════════════════ */
|
|
89
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Load from IndexedDB by storage key
|
|
85
|
+
*/
|
|
90
86
|
fromStorage(key: string): this {
|
|
91
87
|
this._storageKey = key;
|
|
92
|
-
|
|
88
|
+
this._pendingSource = async () => {
|
|
93
89
|
this.state.loading = true;
|
|
94
|
-
this._updateStatus('⏳ Loading...', 'loading');
|
|
95
90
|
try {
|
|
96
|
-
|
|
91
|
+
let df = await this._driver.loadByName(key);
|
|
97
92
|
if (!df) {
|
|
98
93
|
this._triggerCallback('error', 'No table found with key: ' + key, null, this);
|
|
99
94
|
this.state.loading = false;
|
|
100
|
-
this._updateStatus('No table found: ' + key, 'empty');
|
|
101
95
|
return;
|
|
102
96
|
}
|
|
103
97
|
this._setDataFrame(df, 'storage: ' + key);
|
|
104
98
|
} catch (err: any) {
|
|
105
99
|
this._triggerCallback('error', err.message, null, this);
|
|
106
100
|
this.state.loading = false;
|
|
107
|
-
this._updateStatus('❌ ' + err.message, 'error');
|
|
108
101
|
}
|
|
109
102
|
};
|
|
110
|
-
if (this._table) { loadFn(); } else { this._pendingSource = loadFn; }
|
|
111
103
|
return this;
|
|
112
104
|
}
|
|
113
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Load from a FileUpload component — auto-wires change event
|
|
108
|
+
*/
|
|
114
109
|
fromUpload(upload: FileUpload): this {
|
|
115
110
|
this._uploadRef = upload;
|
|
116
111
|
this._pendingSource = async () => {
|
|
112
|
+
// Wire upload's change event to parse incoming files
|
|
117
113
|
upload.bind('change', async (files: File[]) => {
|
|
118
114
|
if (!files || files.length === 0) return;
|
|
119
115
|
const file = files[0];
|
|
120
116
|
this.state.loading = true;
|
|
121
|
-
|
|
117
|
+
|
|
122
118
|
try {
|
|
123
119
|
const df = await this._driver.streamFile(file);
|
|
120
|
+
// Auto-persist to IndexedDB
|
|
124
121
|
await this._driver.store(file.name, df, { source: file.name });
|
|
125
|
-
this._setDataFrame(df, file.name);
|
|
122
|
+
this._setDataFrame(df, 'upload: ' + file.name);
|
|
126
123
|
} catch (err: any) {
|
|
127
124
|
this._triggerCallback('error', err.message, null, this);
|
|
128
125
|
this.state.loading = false;
|
|
129
|
-
this._updateStatus('❌ ' + err.message, 'error');
|
|
130
126
|
}
|
|
131
127
|
});
|
|
132
128
|
};
|
|
133
129
|
return this;
|
|
134
130
|
}
|
|
135
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Load from raw data — array of objects or Record<string, any[]>
|
|
134
|
+
*/
|
|
136
135
|
fromData(data: Record<string, any>[] | Record<string, any[]>): this {
|
|
137
|
-
|
|
136
|
+
this._pendingSource = async () => {
|
|
138
137
|
this.state.loading = true;
|
|
139
|
-
this._updateStatus('⏳ Loading data...', 'loading');
|
|
140
138
|
try {
|
|
141
139
|
const df = new DataFrame(data);
|
|
142
140
|
this._setDataFrame(df, 'inline data');
|
|
143
141
|
} catch (err: any) {
|
|
144
142
|
this._triggerCallback('error', err.message, null, this);
|
|
145
143
|
this.state.loading = false;
|
|
146
|
-
this._updateStatus('❌ ' + err.message, 'error');
|
|
147
144
|
}
|
|
148
145
|
};
|
|
149
|
-
if (this._table) { loadFn(); } else { this._pendingSource = loadFn; }
|
|
150
146
|
return this;
|
|
151
147
|
}
|
|
152
148
|
|
|
153
149
|
/**
|
|
154
|
-
* Add an inline file upload control.
|
|
155
|
-
*
|
|
150
|
+
* Add an inline file upload control above the table.
|
|
151
|
+
* Auto-wires parsing, storage, and display — zero callbacks needed.
|
|
152
|
+
*
|
|
153
|
+
* @param label - Button/label text (default: 'Upload File')
|
|
156
154
|
* @param accept - File types (default: '.csv,.tsv,.txt,.xlsx,.xls')
|
|
157
|
-
* @param icon - Upload icon name (default: 'upload'). Pass '' to hide icon.
|
|
158
155
|
*/
|
|
159
|
-
withUpload(
|
|
160
|
-
|
|
161
|
-
accept: string = '.csv,.tsv,.txt,.xlsx,.xls',
|
|
162
|
-
icon: string = 'upload'
|
|
163
|
-
): this {
|
|
164
|
-
this._inlineUpload = { label, accept, icon };
|
|
156
|
+
withUpload(label: string = 'Upload File', accept: string = '.csv,.tsv,.txt,.xlsx,.xls'): this {
|
|
157
|
+
this._inlineUpload = { label, accept };
|
|
165
158
|
return this;
|
|
166
159
|
}
|
|
167
160
|
|
|
168
161
|
/* ═══════════════════════════════════════════════════
|
|
169
|
-
*
|
|
170
|
-
* ═══════════════════════════════════════════════════ */
|
|
171
|
-
|
|
172
|
-
/** Show/hide the status bar */
|
|
173
|
-
showStatus(v: boolean): this { this._showStatus = v; return this; }
|
|
174
|
-
|
|
175
|
-
/** Set a custom icon for the status bar */
|
|
176
|
-
statusIcon(v: string): this { this._icon = v; return this; }
|
|
177
|
-
|
|
178
|
-
/* ═══════════════════════════════════════════════════
|
|
179
|
-
* TRANSFORM API
|
|
162
|
+
* TRANSFORM API (returns new DataFrameComponent view)
|
|
180
163
|
* ═══════════════════════════════════════════════════ */
|
|
181
164
|
|
|
165
|
+
/**
|
|
166
|
+
* Apply a transform to the current DataFrame and update the table
|
|
167
|
+
*/
|
|
182
168
|
apply(fn: (df: DataFrame) => DataFrame): this {
|
|
183
169
|
if (!this._df) return this;
|
|
184
170
|
const result = fn(this._df);
|
|
@@ -187,30 +173,51 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
187
173
|
return this;
|
|
188
174
|
}
|
|
189
175
|
|
|
176
|
+
/**
|
|
177
|
+
* Filter rows
|
|
178
|
+
*/
|
|
190
179
|
filter(predicate: (row: Record<string, any>, index: number) => boolean): this {
|
|
191
180
|
return this.apply(df => df.filter(predicate));
|
|
192
181
|
}
|
|
193
182
|
|
|
183
|
+
/**
|
|
184
|
+
* Select columns
|
|
185
|
+
*/
|
|
194
186
|
select(...cols: string[]): this {
|
|
195
187
|
return this.apply(df => df.select(...cols));
|
|
196
188
|
}
|
|
197
189
|
|
|
190
|
+
/**
|
|
191
|
+
* Sort by column
|
|
192
|
+
*/
|
|
198
193
|
sort(col: string, descending?: boolean): this {
|
|
199
194
|
return this.apply(df => df.sort(col, descending));
|
|
200
195
|
}
|
|
201
196
|
|
|
197
|
+
/**
|
|
198
|
+
* Show first N rows
|
|
199
|
+
*/
|
|
202
200
|
head(n: number = 5): this {
|
|
203
201
|
return this.apply(df => df.head(n));
|
|
204
202
|
}
|
|
205
203
|
|
|
204
|
+
/**
|
|
205
|
+
* Show last N rows
|
|
206
|
+
*/
|
|
206
207
|
tail(n: number = 5): this {
|
|
207
208
|
return this.apply(df => df.tail(n));
|
|
208
209
|
}
|
|
209
210
|
|
|
211
|
+
/**
|
|
212
|
+
* Add a computed column
|
|
213
|
+
*/
|
|
210
214
|
withColumn(name: string, fn: (row: Record<string, any>, index: number) => any): this {
|
|
211
215
|
return this.apply(df => df.withColumn(name, fn));
|
|
212
216
|
}
|
|
213
217
|
|
|
218
|
+
/**
|
|
219
|
+
* Where clause
|
|
220
|
+
*/
|
|
214
221
|
where(col: string, op: '==' | '!=' | '>' | '<' | '>=' | '<=' | 'contains' | 'startsWith' | 'endsWith', value: any): this {
|
|
215
222
|
return this.apply(df => df.where(col, op, value));
|
|
216
223
|
}
|
|
@@ -219,15 +226,41 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
219
226
|
* ACCESSORS
|
|
220
227
|
* ═══════════════════════════════════════════════════ */
|
|
221
228
|
|
|
229
|
+
/** Get the underlying DataFrame */
|
|
222
230
|
get df(): DataFrame | null { return this._df; }
|
|
231
|
+
|
|
232
|
+
/** Get the underlying TabularDriver */
|
|
223
233
|
get driver(): TabularDriver { return this._driver; }
|
|
234
|
+
|
|
235
|
+
/** Get the internal Table component */
|
|
224
236
|
get table(): Table | null { return this._table; }
|
|
225
|
-
describe(): Record<string, any> | null { return this._df?.describe() ?? null; }
|
|
226
|
-
toCSV(delimiter?: string): string { return this._df?.toCSV(delimiter) ?? ''; }
|
|
227
|
-
toRows(): Record<string, any>[] { return this._df?.toRows() ?? []; }
|
|
228
|
-
get shape(): [number, number] { return this._df?.shape ?? [0, 0]; }
|
|
229
|
-
get columns(): string[] { return this._df?.columns ?? []; }
|
|
230
237
|
|
|
238
|
+
/** Get describe() stats */
|
|
239
|
+
describe(): Record<string, any> | null {
|
|
240
|
+
return this._df?.describe() ?? null;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** Export to CSV string */
|
|
244
|
+
toCSV(delimiter?: string): string {
|
|
245
|
+
return this._df?.toCSV(delimiter) ?? '';
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** Export to row objects */
|
|
249
|
+
toRows(): Record<string, any>[] {
|
|
250
|
+
return this._df?.toRows() ?? [];
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Get shape */
|
|
254
|
+
get shape(): [number, number] {
|
|
255
|
+
return this._df?.shape ?? [0, 0];
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** Get column names */
|
|
259
|
+
get columns(): string[] {
|
|
260
|
+
return this._df?.columns ?? [];
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Save current DataFrame to IndexedDB */
|
|
231
264
|
async save(key?: string): Promise<string | null> {
|
|
232
265
|
if (!this._df) return null;
|
|
233
266
|
const name = key ?? this._storageKey ?? this._id;
|
|
@@ -249,28 +282,9 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
249
282
|
* INTERNAL
|
|
250
283
|
* ═══════════════════════════════════════════════════ */
|
|
251
284
|
|
|
252
|
-
private _updateStatus(text: string
|
|
285
|
+
private _updateStatus(text: string): void {
|
|
253
286
|
const el = document.getElementById(`${this._id}-status`);
|
|
254
|
-
if (
|
|
255
|
-
|
|
256
|
-
el.className = 'jux-dataframe-status';
|
|
257
|
-
if (type) el.classList.add(`jux-dataframe-status-${type}`);
|
|
258
|
-
|
|
259
|
-
// Clear and rebuild
|
|
260
|
-
el.innerHTML = '';
|
|
261
|
-
|
|
262
|
-
if (this._icon && type === 'success') {
|
|
263
|
-
const iconEl = renderIcon(this._icon);
|
|
264
|
-
iconEl.style.width = '16px';
|
|
265
|
-
iconEl.style.height = '16px';
|
|
266
|
-
iconEl.style.marginRight = '6px';
|
|
267
|
-
iconEl.style.verticalAlign = 'middle';
|
|
268
|
-
el.appendChild(iconEl);
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
const span = document.createElement('span');
|
|
272
|
-
span.textContent = text;
|
|
273
|
-
el.appendChild(span);
|
|
287
|
+
if (el) el.textContent = text;
|
|
274
288
|
}
|
|
275
289
|
|
|
276
290
|
private _setDataFrame(df: DataFrame, sourceName: string): void {
|
|
@@ -287,17 +301,13 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
287
301
|
}
|
|
288
302
|
|
|
289
303
|
this._updateTable();
|
|
290
|
-
this._updateStatus(
|
|
291
|
-
|
|
292
|
-
'success'
|
|
293
|
-
);
|
|
304
|
+
this._updateStatus(`✅ ${sourceName} — ${this._df!.height} rows × ${this._df!.width} cols`);
|
|
305
|
+
this._triggerCallback('load', this._df, null, this);
|
|
294
306
|
|
|
295
|
-
// Enable filter now that data exists
|
|
307
|
+
// ✅ Enable filter now that data exists (render it into DOM)
|
|
296
308
|
if (this._tableOptions.filterable) {
|
|
297
309
|
this._showFilterInput();
|
|
298
310
|
}
|
|
299
|
-
|
|
300
|
-
this._triggerCallback('load', this._df, null, this);
|
|
301
311
|
}
|
|
302
312
|
|
|
303
313
|
private _updateTable(): void {
|
|
@@ -305,9 +315,16 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
305
315
|
this._table.columns(this._df.columns).rows(this._df.toRows());
|
|
306
316
|
}
|
|
307
317
|
|
|
318
|
+
update(prop: string, value: any): void { }
|
|
319
|
+
|
|
320
|
+
/* ═══════════════════════════════════════════════════
|
|
321
|
+
* RENDER
|
|
322
|
+
* ═══════════════════════════════════════════════════ */
|
|
323
|
+
|
|
308
324
|
private _showFilterInput(): void {
|
|
309
325
|
const wrapper = document.getElementById(this._id);
|
|
310
326
|
if (!wrapper) return;
|
|
327
|
+
|
|
311
328
|
// Only add once
|
|
312
329
|
if (wrapper.querySelector('.jux-dataframe-filter')) return;
|
|
313
330
|
|
|
@@ -330,11 +347,14 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
330
347
|
filterContainer.appendChild(iconWrap);
|
|
331
348
|
filterContainer.appendChild(input);
|
|
332
349
|
|
|
333
|
-
//
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
|
|
350
|
+
// ✅ FIX: Find the table element (we know it exists because render() created it)
|
|
351
|
+
const tableElement = wrapper.querySelector('.jux-table-wrapper table');
|
|
352
|
+
|
|
353
|
+
if (tableElement && tableElement.parentElement) {
|
|
354
|
+
// Insert filter BEFORE the table element
|
|
355
|
+
tableElement.parentElement.insertBefore(filterContainer, tableElement);
|
|
337
356
|
} else {
|
|
357
|
+
// Fallback: append to wrapper (shouldn't happen, but safe)
|
|
338
358
|
wrapper.appendChild(filterContainer);
|
|
339
359
|
}
|
|
340
360
|
|
|
@@ -354,12 +374,6 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
354
374
|
});
|
|
355
375
|
}
|
|
356
376
|
|
|
357
|
-
update(prop: string, value: any): void { }
|
|
358
|
-
|
|
359
|
-
/* ═══════════════════════════════════════════════════
|
|
360
|
-
* RENDER
|
|
361
|
-
* ═══════════════════════════════════════════════════ */
|
|
362
|
-
|
|
363
377
|
render(targetId?: string | HTMLElement | BaseComponent<any>): this {
|
|
364
378
|
const container = this._setupContainer(targetId);
|
|
365
379
|
const { style, class: className } = this.state;
|
|
@@ -370,25 +384,22 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
370
384
|
if (className) wrapper.className += ` ${className}`;
|
|
371
385
|
if (style) wrapper.setAttribute('style', style);
|
|
372
386
|
|
|
373
|
-
// Inline upload
|
|
387
|
+
// Inline upload (if withUpload was called)
|
|
374
388
|
if (this._inlineUpload) {
|
|
375
|
-
const
|
|
389
|
+
const upload = new FileUpload(`${this._id}-upload`, {
|
|
376
390
|
label: this._inlineUpload.label,
|
|
377
|
-
accept: this._inlineUpload.accept
|
|
378
|
-
};
|
|
379
|
-
if (this._inlineUpload.icon) {
|
|
380
|
-
uploadOpts.icon = this._inlineUpload.icon;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
const upload = new FileUpload(`${this._id}-upload`, uploadOpts);
|
|
391
|
+
accept: this._inlineUpload.accept
|
|
392
|
+
});
|
|
384
393
|
|
|
394
|
+
// Wire it as a fromUpload source
|
|
385
395
|
this._uploadRef = upload;
|
|
386
396
|
this._pendingSource = async () => {
|
|
387
397
|
upload.bind('change', async (files: File[]) => {
|
|
388
398
|
if (!files || files.length === 0) return;
|
|
389
399
|
const file = files[0];
|
|
390
400
|
this.state.loading = true;
|
|
391
|
-
this._updateStatus('⏳ Parsing ' + file.name + '...'
|
|
401
|
+
this._updateStatus('⏳ Parsing ' + file.name + '...');
|
|
402
|
+
|
|
392
403
|
try {
|
|
393
404
|
const df = await this._driver.streamFile(file);
|
|
394
405
|
await this._driver.store(file.name, df, { source: file.name });
|
|
@@ -396,11 +407,12 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
396
407
|
} catch (err: any) {
|
|
397
408
|
this._triggerCallback('error', err.message, null, this);
|
|
398
409
|
this.state.loading = false;
|
|
399
|
-
this._updateStatus('❌ ' + err.message
|
|
410
|
+
this._updateStatus('❌ ' + err.message);
|
|
400
411
|
}
|
|
401
412
|
});
|
|
402
413
|
};
|
|
403
414
|
|
|
415
|
+
// Render upload into wrapper before status/table
|
|
404
416
|
const uploadContainer = document.createElement('div');
|
|
405
417
|
uploadContainer.className = 'jux-dataframe-upload';
|
|
406
418
|
uploadContainer.id = `${this._id}-upload-container`;
|
|
@@ -411,16 +423,15 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
411
423
|
container.appendChild(wrapper);
|
|
412
424
|
}
|
|
413
425
|
|
|
414
|
-
// Status bar
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
}
|
|
426
|
+
// Status bar
|
|
427
|
+
const statusBar = document.createElement('div');
|
|
428
|
+
statusBar.className = 'jux-dataframe-status';
|
|
429
|
+
statusBar.id = `${this._id}-status`;
|
|
430
|
+
statusBar.style.cssText = 'font-size:13px;color:#888;margin-bottom:8px;';
|
|
431
|
+
statusBar.textContent = 'No data loaded.';
|
|
432
|
+
wrapper.appendChild(statusBar);
|
|
422
433
|
|
|
423
|
-
//
|
|
434
|
+
// ✅ Create internal table — this establishes the DOM structure
|
|
424
435
|
const tbl = new Table(`${this._id}-table`, {
|
|
425
436
|
striped: this._tableOptions.striped,
|
|
426
437
|
hoverable: this._tableOptions.hoverable,
|
|
@@ -429,7 +440,7 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
429
440
|
paginated: this._tableOptions.paginated,
|
|
430
441
|
rowsPerPage: this._tableOptions.rowsPerPage
|
|
431
442
|
});
|
|
432
|
-
tbl.render(wrapper);
|
|
443
|
+
tbl.render(wrapper); // ✅ Table now exists in wrapper
|
|
433
444
|
this._table = tbl;
|
|
434
445
|
|
|
435
446
|
// Execute pending data source
|
|
@@ -440,6 +451,7 @@ export class DataFrameComponent extends BaseComponent<DataFrameState> {
|
|
|
440
451
|
}
|
|
441
452
|
|
|
442
453
|
this._wireStandardEvents(wrapper);
|
|
454
|
+
|
|
443
455
|
return this;
|
|
444
456
|
}
|
|
445
457
|
}
|