juxscript 1.1.165 → 1.1.167

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.
@@ -12,6 +12,8 @@ export interface DataFrameOptions {
12
12
  filterable?: boolean;
13
13
  paginated?: boolean;
14
14
  rowsPerPage?: number;
15
+ showStatus?: boolean;
16
+ icon?: string;
15
17
  style?: string;
16
18
  class?: string;
17
19
  }
@@ -29,70 +31,42 @@ export declare class DataFrameComponent extends BaseComponent<DataFrameState> {
29
31
  private _uploadRef;
30
32
  private _storageKey;
31
33
  private _pendingSource;
34
+ private _inlineUpload;
35
+ private _showStatus;
36
+ private _icon;
32
37
  constructor(id: string, options?: DataFrameOptions);
33
38
  protected getTriggerEvents(): readonly string[];
34
39
  protected getCallbackEvents(): readonly string[];
35
- /**
36
- * Load from IndexedDB by storage key
37
- */
38
40
  fromStorage(key: string): this;
39
- /**
40
- * Load from a FileUpload component — auto-wires change event
41
- */
42
41
  fromUpload(upload: FileUpload): this;
43
- /**
44
- * Load from raw data — array of objects or Record<string, any[]>
45
- */
46
42
  fromData(data: Record<string, any>[] | Record<string, any[]>): this;
47
43
  /**
48
- * Apply a transform to the current DataFrame and update the table
44
+ * Add an inline file upload control.
45
+ * @param label - Button label (default: 'Upload File')
46
+ * @param accept - File types (default: '.csv,.tsv,.txt,.xlsx,.xls')
47
+ * @param icon - Upload icon name (default: 'upload'). Pass '' to hide icon.
49
48
  */
49
+ withUpload(label?: string, accept?: string, icon?: string): this;
50
+ /** Show/hide the status bar */
51
+ showStatus(v: boolean): this;
52
+ /** Set a custom icon for the status bar */
53
+ statusIcon(v: string): this;
50
54
  apply(fn: (df: DataFrame) => DataFrame): this;
51
- /**
52
- * Filter rows
53
- */
54
55
  filter(predicate: (row: Record<string, any>, index: number) => boolean): this;
55
- /**
56
- * Select columns
57
- */
58
56
  select(...cols: string[]): this;
59
- /**
60
- * Sort by column
61
- */
62
57
  sort(col: string, descending?: boolean): this;
63
- /**
64
- * Show first N rows
65
- */
66
58
  head(n?: number): this;
67
- /**
68
- * Show last N rows
69
- */
70
59
  tail(n?: number): this;
71
- /**
72
- * Add a computed column
73
- */
74
60
  withColumn(name: string, fn: (row: Record<string, any>, index: number) => any): this;
75
- /**
76
- * Where clause
77
- */
78
61
  where(col: string, op: '==' | '!=' | '>' | '<' | '>=' | '<=' | 'contains' | 'startsWith' | 'endsWith', value: any): this;
79
- /** Get the underlying DataFrame */
80
62
  get df(): DataFrame | null;
81
- /** Get the underlying TabularDriver */
82
63
  get driver(): TabularDriver;
83
- /** Get the internal Table component */
84
64
  get table(): Table | null;
85
- /** Get describe() stats */
86
65
  describe(): Record<string, any> | null;
87
- /** Export to CSV string */
88
66
  toCSV(delimiter?: string): string;
89
- /** Export to row objects */
90
67
  toRows(): Record<string, any>[];
91
- /** Get shape */
92
68
  get shape(): [number, number];
93
- /** Get column names */
94
69
  get columns(): string[];
95
- /** Save current DataFrame to IndexedDB */
96
70
  save(key?: string): Promise<string | null>;
97
71
  striped(v: boolean): this;
98
72
  hoverable(v: boolean): this;
@@ -100,8 +74,10 @@ export declare class DataFrameComponent extends BaseComponent<DataFrameState> {
100
74
  filterable(v: boolean): this;
101
75
  paginated(v: boolean): this;
102
76
  rowsPerPage(v: number): this;
77
+ private _updateStatus;
103
78
  private _setDataFrame;
104
79
  private _updateTable;
80
+ private _showFilterInput;
105
81
  update(prop: string, value: any): void;
106
82
  render(targetId?: string | HTMLElement | BaseComponent<any>): this;
107
83
  }
@@ -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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"}
@@ -1,7 +1,9 @@
1
1
  import { BaseComponent } from './base/BaseComponent.js';
2
2
  import { DataFrame } from '../storage/DataFrame.js';
3
3
  import { TabularDriver } from '../storage/TabularDriver.js';
4
+ import { FileUpload } from './fileupload.js';
4
5
  import { Table } from './table.js';
6
+ import { renderIcon } from './icons.js';
5
7
  const TRIGGER_EVENTS = [];
6
8
  const CALLBACK_EVENTS = ['load', 'error', 'transform'];
7
9
  export class DataFrameComponent extends BaseComponent {
@@ -22,12 +24,17 @@ export class DataFrameComponent extends BaseComponent {
22
24
  this._uploadRef = null;
23
25
  this._storageKey = null;
24
26
  this._pendingSource = null;
27
+ this._inlineUpload = null;
28
+ this._showStatus = true;
29
+ this._icon = '';
25
30
  this._driver = new TabularDriver(options.dbName ?? 'jux-dataframes', options.storeName ?? 'frames');
31
+ this._showStatus = options.showStatus ?? true;
32
+ this._icon = options.icon ?? '';
26
33
  this._tableOptions = {
27
34
  striped: options.striped ?? true,
28
35
  hoverable: options.hoverable ?? true,
29
36
  sortable: options.sortable ?? true,
30
- filterable: options.filterable ?? true,
37
+ filterable: options.filterable ?? false, // defer until data loaded
31
38
  paginated: options.paginated ?? true,
32
39
  rowsPerPage: options.rowsPerPage ?? 25
33
40
  };
@@ -37,18 +44,17 @@ export class DataFrameComponent extends BaseComponent {
37
44
  /* ═══════════════════════════════════════════════════
38
45
  * DATA SOURCES
39
46
  * ═══════════════════════════════════════════════════ */
40
- /**
41
- * Load from IndexedDB by storage key
42
- */
43
47
  fromStorage(key) {
44
48
  this._storageKey = key;
45
- this._pendingSource = async () => {
49
+ const loadFn = async () => {
46
50
  this.state.loading = true;
51
+ this._updateStatus('⏳ Loading...', 'loading');
47
52
  try {
48
- let df = await this._driver.loadByName(key);
53
+ const df = await this._driver.loadByName(key);
49
54
  if (!df) {
50
55
  this._triggerCallback('error', 'No table found with key: ' + key, null, this);
51
56
  this.state.loading = false;
57
+ this._updateStatus('No table found: ' + key, 'empty');
52
58
  return;
53
59
  }
54
60
  this._setDataFrame(df, 'storage: ' + key);
@@ -56,42 +62,44 @@ export class DataFrameComponent extends BaseComponent {
56
62
  catch (err) {
57
63
  this._triggerCallback('error', err.message, null, this);
58
64
  this.state.loading = false;
65
+ this._updateStatus('❌ ' + err.message, 'error');
59
66
  }
60
67
  };
68
+ if (this._table) {
69
+ loadFn();
70
+ }
71
+ else {
72
+ this._pendingSource = loadFn;
73
+ }
61
74
  return this;
62
75
  }
63
- /**
64
- * Load from a FileUpload component — auto-wires change event
65
- */
66
76
  fromUpload(upload) {
67
77
  this._uploadRef = upload;
68
78
  this._pendingSource = async () => {
69
- // Wire upload's change event to parse incoming files
70
79
  upload.bind('change', async (files) => {
71
80
  if (!files || files.length === 0)
72
81
  return;
73
82
  const file = files[0];
74
83
  this.state.loading = true;
84
+ this._updateStatus('⏳ Parsing ' + file.name + '...', 'loading');
75
85
  try {
76
86
  const df = await this._driver.streamFile(file);
77
- // Auto-persist to IndexedDB
78
87
  await this._driver.store(file.name, df, { source: file.name });
79
- this._setDataFrame(df, 'upload: ' + file.name);
88
+ this._setDataFrame(df, file.name);
80
89
  }
81
90
  catch (err) {
82
91
  this._triggerCallback('error', err.message, null, this);
83
92
  this.state.loading = false;
93
+ this._updateStatus('❌ ' + err.message, 'error');
84
94
  }
85
95
  });
86
96
  };
87
97
  return this;
88
98
  }
89
- /**
90
- * Load from raw data — array of objects or Record<string, any[]>
91
- */
92
99
  fromData(data) {
93
- this._pendingSource = async () => {
100
+ const loadFn = async () => {
94
101
  this.state.loading = true;
102
+ this._updateStatus('⏳ Loading data...', 'loading');
95
103
  try {
96
104
  const df = new DataFrame(data);
97
105
  this._setDataFrame(df, 'inline data');
@@ -99,16 +107,37 @@ export class DataFrameComponent extends BaseComponent {
99
107
  catch (err) {
100
108
  this._triggerCallback('error', err.message, null, this);
101
109
  this.state.loading = false;
110
+ this._updateStatus('❌ ' + err.message, 'error');
102
111
  }
103
112
  };
113
+ if (this._table) {
114
+ loadFn();
115
+ }
116
+ else {
117
+ this._pendingSource = loadFn;
118
+ }
104
119
  return this;
105
120
  }
106
- /* ═══════════════════════════════════════════════════
107
- * TRANSFORM API (returns new DataFrameComponent view)
108
- * ═══════════════════════════════════════════════════ */
109
121
  /**
110
- * Apply a transform to the current DataFrame and update the table
122
+ * Add an inline file upload control.
123
+ * @param label - Button label (default: 'Upload File')
124
+ * @param accept - File types (default: '.csv,.tsv,.txt,.xlsx,.xls')
125
+ * @param icon - Upload icon name (default: 'upload'). Pass '' to hide icon.
111
126
  */
127
+ withUpload(label = 'Upload File', accept = '.csv,.tsv,.txt,.xlsx,.xls', icon = 'upload') {
128
+ this._inlineUpload = { label, accept, icon };
129
+ return this;
130
+ }
131
+ /* ═══════════════════════════════════════════════════
132
+ * UI TOGGLES
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
140
+ * ═══════════════════════════════════════════════════ */
112
141
  apply(fn) {
113
142
  if (!this._df)
114
143
  return this;
@@ -117,78 +146,38 @@ export class DataFrameComponent extends BaseComponent {
117
146
  this._triggerCallback('transform', result, null, this);
118
147
  return this;
119
148
  }
120
- /**
121
- * Filter rows
122
- */
123
149
  filter(predicate) {
124
150
  return this.apply(df => df.filter(predicate));
125
151
  }
126
- /**
127
- * Select columns
128
- */
129
152
  select(...cols) {
130
153
  return this.apply(df => df.select(...cols));
131
154
  }
132
- /**
133
- * Sort by column
134
- */
135
155
  sort(col, descending) {
136
156
  return this.apply(df => df.sort(col, descending));
137
157
  }
138
- /**
139
- * Show first N rows
140
- */
141
158
  head(n = 5) {
142
159
  return this.apply(df => df.head(n));
143
160
  }
144
- /**
145
- * Show last N rows
146
- */
147
161
  tail(n = 5) {
148
162
  return this.apply(df => df.tail(n));
149
163
  }
150
- /**
151
- * Add a computed column
152
- */
153
164
  withColumn(name, fn) {
154
165
  return this.apply(df => df.withColumn(name, fn));
155
166
  }
156
- /**
157
- * Where clause
158
- */
159
167
  where(col, op, value) {
160
168
  return this.apply(df => df.where(col, op, value));
161
169
  }
162
170
  /* ═══════════════════════════════════════════════════
163
171
  * ACCESSORS
164
172
  * ═══════════════════════════════════════════════════ */
165
- /** Get the underlying DataFrame */
166
173
  get df() { return this._df; }
167
- /** Get the underlying TabularDriver */
168
174
  get driver() { return this._driver; }
169
- /** Get the internal Table component */
170
175
  get table() { return this._table; }
171
- /** Get describe() stats */
172
- describe() {
173
- return this._df?.describe() ?? null;
174
- }
175
- /** Export to CSV string */
176
- toCSV(delimiter) {
177
- return this._df?.toCSV(delimiter) ?? '';
178
- }
179
- /** Export to row objects */
180
- toRows() {
181
- return this._df?.toRows() ?? [];
182
- }
183
- /** Get shape */
184
- get shape() {
185
- return this._df?.shape ?? [0, 0];
186
- }
187
- /** Get column names */
188
- get columns() {
189
- return this._df?.columns ?? [];
190
- }
191
- /** Save current DataFrame to IndexedDB */
176
+ describe() { return this._df?.describe() ?? null; }
177
+ toCSV(delimiter) { return this._df?.toCSV(delimiter) ?? ''; }
178
+ toRows() { return this._df?.toRows() ?? []; }
179
+ get shape() { return this._df?.shape ?? [0, 0]; }
180
+ get columns() { return this._df?.columns ?? []; }
192
181
  async save(key) {
193
182
  if (!this._df)
194
183
  return null;
@@ -207,6 +196,27 @@ export class DataFrameComponent extends BaseComponent {
207
196
  /* ═══════════════════════════════════════════════════
208
197
  * INTERNAL
209
198
  * ═══════════════════════════════════════════════════ */
199
+ _updateStatus(text, type = 'empty') {
200
+ const el = document.getElementById(`${this._id}-status`);
201
+ if (!el)
202
+ return;
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);
219
+ }
210
220
  _setDataFrame(df, sourceName) {
211
221
  this._df = df;
212
222
  this.state.loaded = true;
@@ -214,12 +224,16 @@ export class DataFrameComponent extends BaseComponent {
214
224
  this.state.sourceName = sourceName;
215
225
  this.state.rowCount = df.height;
216
226
  this.state.colCount = df.width;
217
- // Clean __EMPTY columns from xlsx artifacts
218
227
  const cleanCols = df.columns.filter(c => !c.startsWith('__EMPTY'));
219
228
  if (cleanCols.length < df.columns.length) {
220
229
  this._df = df.select(...cleanCols);
221
230
  }
222
231
  this._updateTable();
232
+ this._updateStatus(`${sourceName} — ${this._df.height} rows × ${this._df.width} cols`, 'success');
233
+ // Enable filter now that data exists
234
+ if (this._tableOptions.filterable) {
235
+ this._showFilterInput();
236
+ }
223
237
  this._triggerCallback('load', this._df, null, this);
224
238
  }
225
239
  _updateTable() {
@@ -227,6 +241,49 @@ export class DataFrameComponent extends BaseComponent {
227
241
  return;
228
242
  this._table.columns(this._df.columns).rows(this._df.toRows());
229
243
  }
244
+ _showFilterInput() {
245
+ const wrapper = document.getElementById(this._id);
246
+ if (!wrapper)
247
+ return;
248
+ // Only add once
249
+ if (wrapper.querySelector('.jux-dataframe-filter'))
250
+ return;
251
+ const filterContainer = document.createElement('div');
252
+ filterContainer.className = 'jux-dataframe-filter';
253
+ const input = document.createElement('input');
254
+ input.type = 'text';
255
+ input.placeholder = 'Filter rows...';
256
+ input.className = 'jux-input-element jux-dataframe-filter-input';
257
+ const iconEl = renderIcon('search');
258
+ iconEl.style.width = '16px';
259
+ iconEl.style.height = '16px';
260
+ const iconWrap = document.createElement('span');
261
+ iconWrap.className = 'jux-dataframe-filter-icon';
262
+ iconWrap.appendChild(iconEl);
263
+ filterContainer.appendChild(iconWrap);
264
+ filterContainer.appendChild(input);
265
+ // Insert before the table
266
+ const tableWrapper = wrapper.querySelector('.jux-table-wrapper');
267
+ if (tableWrapper) {
268
+ wrapper.insertBefore(filterContainer, tableWrapper);
269
+ }
270
+ else {
271
+ wrapper.appendChild(filterContainer);
272
+ }
273
+ input.addEventListener('input', () => {
274
+ if (!this._df)
275
+ return;
276
+ const text = input.value.toLowerCase();
277
+ if (!text) {
278
+ this._table?.rows(this._df.toRows());
279
+ return;
280
+ }
281
+ const filtered = this._df.filter((row) => {
282
+ return Object.values(row).some(v => v !== null && v !== undefined && String(v).toLowerCase().includes(text));
283
+ });
284
+ this._table?.rows(filtered.toRows());
285
+ });
286
+ }
230
287
  update(prop, value) { }
231
288
  /* ═══════════════════════════════════════════════════
232
289
  * RENDER
@@ -241,47 +298,70 @@ export class DataFrameComponent extends BaseComponent {
241
298
  wrapper.className += ` ${className}`;
242
299
  if (style)
243
300
  wrapper.setAttribute('style', style);
244
- // Status bar
245
- const statusBar = document.createElement('div');
246
- statusBar.className = 'jux-dataframe-status';
247
- statusBar.id = `${this._id}-status`;
248
- statusBar.style.cssText = 'font-size:13px;color:#888;margin-bottom:8px;';
249
- statusBar.textContent = 'No data loaded.';
250
- wrapper.appendChild(statusBar);
251
- container.appendChild(wrapper);
252
- // Create internal table
253
- this._table = new Table(`${this._id}-table`, {
301
+ // Inline upload
302
+ if (this._inlineUpload) {
303
+ const uploadOpts = {
304
+ label: this._inlineUpload.label,
305
+ accept: this._inlineUpload.accept,
306
+ };
307
+ if (this._inlineUpload.icon) {
308
+ uploadOpts.icon = this._inlineUpload.icon;
309
+ }
310
+ const upload = new FileUpload(`${this._id}-upload`, uploadOpts);
311
+ this._uploadRef = upload;
312
+ this._pendingSource = async () => {
313
+ upload.bind('change', async (files) => {
314
+ if (!files || files.length === 0)
315
+ return;
316
+ const file = files[0];
317
+ this.state.loading = true;
318
+ this._updateStatus('⏳ Parsing ' + file.name + '...', 'loading');
319
+ try {
320
+ const df = await this._driver.streamFile(file);
321
+ await this._driver.store(file.name, df, { source: file.name });
322
+ this._setDataFrame(df, file.name);
323
+ }
324
+ catch (err) {
325
+ this._triggerCallback('error', err.message, null, this);
326
+ this.state.loading = false;
327
+ this._updateStatus('❌ ' + err.message, 'error');
328
+ }
329
+ });
330
+ };
331
+ const uploadContainer = document.createElement('div');
332
+ uploadContainer.className = 'jux-dataframe-upload';
333
+ uploadContainer.id = `${this._id}-upload-container`;
334
+ wrapper.appendChild(uploadContainer);
335
+ container.appendChild(wrapper);
336
+ upload.render(uploadContainer);
337
+ }
338
+ else {
339
+ container.appendChild(wrapper);
340
+ }
341
+ // Status bar (conditional)
342
+ if (this._showStatus) {
343
+ const statusBar = document.createElement('div');
344
+ statusBar.className = 'jux-dataframe-status jux-dataframe-status-empty';
345
+ statusBar.id = `${this._id}-status`;
346
+ statusBar.textContent = 'No data loaded.';
347
+ wrapper.appendChild(statusBar);
348
+ }
349
+ // Table — filterable is false initially; we enable it after data loads
350
+ const tbl = new Table(`${this._id}-table`, {
254
351
  striped: this._tableOptions.striped,
255
352
  hoverable: this._tableOptions.hoverable,
256
353
  sortable: this._tableOptions.sortable,
257
- filterable: this._tableOptions.filterable,
354
+ filterable: false, // we handle filtering ourselves
258
355
  paginated: this._tableOptions.paginated,
259
356
  rowsPerPage: this._tableOptions.rowsPerPage
260
357
  });
261
- this._table.render(wrapper);
262
- // Watch state for status updates
263
- const origUpdate = this.update.bind(this);
264
- this.update = (prop, value) => {
265
- origUpdate(prop, value);
266
- if (prop === 'loaded' || prop === 'loading' || prop === 'rowCount' || prop === 'colCount' || prop === 'sourceName') {
267
- const el = document.getElementById(`${this._id}-status`);
268
- if (el) {
269
- if (this.state.loading) {
270
- el.textContent = '⏳ Loading...';
271
- }
272
- else if (this.state.loaded) {
273
- el.textContent = `✅ ${this.state.sourceName} — ${this.state.rowCount} rows × ${this.state.colCount} cols`;
274
- }
275
- else {
276
- el.textContent = 'No data loaded.';
277
- }
278
- }
279
- }
280
- };
358
+ tbl.render(wrapper);
359
+ this._table = tbl;
281
360
  // Execute pending data source
282
361
  if (this._pendingSource) {
283
- this._pendingSource();
362
+ const fn = this._pendingSource;
284
363
  this._pendingSource = null;
364
+ fn();
285
365
  }
286
366
  this._wireStandardEvents(wrapper);
287
367
  return this;