juxscript 1.1.232 → 1.1.234

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.
@@ -0,0 +1,388 @@
1
+ import { DataFrame } from '../storage/DataFrame.js';
2
+ import { TabularDriver } from '../storage/TabularDriver.js';
3
+
4
+ export interface PipelineOptions {
5
+ dbName?: string;
6
+ storeName?: string;
7
+ }
8
+
9
+ export interface FetchOptions {
10
+ delimiter?: string;
11
+ hasHeader?: boolean;
12
+ headerRow?: number;
13
+ mimeType?: 'csv' | 'tsv' | 'json' | 'auto';
14
+ }
15
+
16
+ /**
17
+ * DataPipeline — Headless, promise-based data loading and transformation.
18
+ *
19
+ * Usage:
20
+ * const df = await data.fromUrl('https://example.com/report.csv').result();
21
+ * const df = await data.fromStorage('patients').result();
22
+ * const df = await data.fromJson(apiResponse).result();
23
+ *
24
+ * // Transform and distribute:
25
+ * await data.fromUrl(url)
26
+ * .transform(df => df.select('name', 'age').filter(r => r.age > 30))
27
+ * .into(myTable);
28
+ *
29
+ * await data.fromStorage('encounters')
30
+ * .transform(df => df.distinct('provider'))
31
+ * .into(myDropdown, { labelKey: 'provider', valueKey: 'provider' });
32
+ */
33
+ export class DataPipeline {
34
+ private _driver: TabularDriver;
35
+ private _pending: Promise<DataFrame> | null = null;
36
+ private _transforms: ((df: DataFrame) => DataFrame)[] = [];
37
+
38
+ constructor(options: PipelineOptions = {}) {
39
+ this._driver = new TabularDriver(
40
+ options.dbName ?? 'jux-dataframes',
41
+ options.storeName ?? 'frames'
42
+ );
43
+ }
44
+
45
+ /* ═══════════════════════════════════════════════════
46
+ * SOURCES
47
+ * ═══════════════════════════════════════════════════ */
48
+
49
+ /**
50
+ * Load from IndexedDB by key
51
+ */
52
+ fromStorage(key: string): this {
53
+ this._transforms = [];
54
+ this._pending = this._driver.loadByName(key).then(df => {
55
+ if (!df) throw new Error(`No data found in storage with key: "${key}"`);
56
+ return df;
57
+ });
58
+ return this;
59
+ }
60
+
61
+ /**
62
+ * Load from a URL (CSV, TSV, or JSON)
63
+ */
64
+ fromUrl(url: string, options: FetchOptions = {}): this {
65
+ this._transforms = [];
66
+ this._pending = this._loadFromUrl(url, options);
67
+ return this;
68
+ }
69
+
70
+ /**
71
+ * Load from a File object directly
72
+ */
73
+ fromFile(file: File, options: FetchOptions = {}): this {
74
+ this._transforms = [];
75
+ this._pending = this._loadFromFile(file, options);
76
+ return this;
77
+ }
78
+
79
+ /**
80
+ * Load from an array of row objects
81
+ */
82
+ fromRows(rows: Record<string, any>[], columns?: string[]): this {
83
+ this._transforms = [];
84
+ this._pending = Promise.resolve(new DataFrame(rows, { columns }));
85
+ return this;
86
+ }
87
+
88
+ /**
89
+ * Load from column-oriented data
90
+ */
91
+ fromColumns(columns: Record<string, any[]>): this {
92
+ this._transforms = [];
93
+ this._pending = Promise.resolve(new DataFrame(columns));
94
+ return this;
95
+ }
96
+
97
+ /**
98
+ * Load from a JSON response (array of objects or column-oriented)
99
+ */
100
+ fromJson(json: Record<string, any>[] | Record<string, any[]> | string): this {
101
+ this._transforms = [];
102
+ const parsed = typeof json === 'string' ? JSON.parse(json) : json;
103
+ this._pending = Promise.resolve(new DataFrame(parsed));
104
+ return this;
105
+ }
106
+
107
+ /**
108
+ * Load from a FileUpload component's cached storage key.
109
+ * Requires .cache() enabled on the FileUpload and a file already uploaded.
110
+ *
111
+ * Usage:
112
+ * await data().fromUpload(myUpload).select('name').into(myTable);
113
+ */
114
+ fromUpload(upload: any): this {
115
+ this._transforms = [];
116
+ const key = upload.storageKey;
117
+ if (!key) {
118
+ this._pending = Promise.reject(
119
+ new Error('FileUpload has no cached storage key. Ensure .cache() is enabled and a file has been uploaded.')
120
+ );
121
+ } else {
122
+ this._pending = this._driver.loadByName(key).then((df: any) => {
123
+ if (!df) throw new Error(`No cached data found for key: "${key}"`);
124
+ return df;
125
+ });
126
+ }
127
+ return this;
128
+ }
129
+
130
+ /**
131
+ * Start from an existing DataFrame
132
+ */
133
+ from(df: DataFrame): this {
134
+ this._transforms = [];
135
+ this._pending = Promise.resolve(df.clone());
136
+ return this;
137
+ }
138
+
139
+ /* ═══════════════════════════════════════════════════
140
+ * TRANSFORMS (chained, lazy — applied on .result())
141
+ * ═══════════════════════════════════════════════════ */
142
+
143
+ /**
144
+ * Apply an arbitrary transform
145
+ */
146
+ transform(fn: (df: DataFrame) => DataFrame): this {
147
+ this._transforms.push(fn);
148
+ return this;
149
+ }
150
+
151
+ /**
152
+ * Select columns
153
+ */
154
+ select(...cols: string[]): this {
155
+ return this.transform(df => df.select(...cols));
156
+ }
157
+
158
+ /**
159
+ * Filter rows
160
+ */
161
+ filter(predicate: (row: Record<string, any>, index: number) => boolean): this {
162
+ return this.transform(df => df.filter(predicate));
163
+ }
164
+
165
+ /**
166
+ * Sort by column
167
+ */
168
+ sort(column: string, descending?: boolean): this {
169
+ return this.transform(df => df.sort(column, descending));
170
+ }
171
+
172
+ /**
173
+ * Take first N rows
174
+ */
175
+ head(n: number = 5): this {
176
+ return this.transform(df => df.head(n));
177
+ }
178
+
179
+ /**
180
+ * Take last N rows
181
+ */
182
+ tail(n: number = 5): this {
183
+ return this.transform(df => df.tail(n));
184
+ }
185
+
186
+ /**
187
+ * Rename columns
188
+ */
189
+ rename(mapping: Record<string, string>): this {
190
+ return this.transform(df => df.rename(mapping));
191
+ }
192
+
193
+ /**
194
+ * Add a computed column
195
+ */
196
+ withColumn(name: string, fn: (row: Record<string, any>, index: number) => any): this {
197
+ return this.transform(df => df.withColumn(name, fn));
198
+ }
199
+
200
+ /**
201
+ * Drop nulls
202
+ */
203
+ dropna(columns?: string[]): this {
204
+ return this.transform(df => df.dropna(columns));
205
+ }
206
+
207
+ /**
208
+ * Drop duplicates
209
+ */
210
+ dropDuplicates(columns?: string[]): this {
211
+ return this.transform(df => df.dropDuplicates(columns));
212
+ }
213
+
214
+ /* ═══════════════════════════════════════════════════
215
+ * TERMINAL OPERATIONS
216
+ * ═══════════════════════════════════════════════════ */
217
+
218
+ /**
219
+ * Execute the pipeline and return the DataFrame
220
+ */
221
+ async result(): Promise<DataFrame> {
222
+ if (!this._pending) throw new Error('No data source specified. Call fromStorage(), fromUrl(), etc. first.');
223
+
224
+ let df = await this._pending;
225
+ for (const fn of this._transforms) {
226
+ df = fn(df);
227
+ }
228
+ return df;
229
+ }
230
+
231
+ /**
232
+ * Execute and return rows as plain objects
233
+ */
234
+ async toRows(): Promise<Record<string, any>[]> {
235
+ return (await this.result()).toRows();
236
+ }
237
+
238
+ /**
239
+ * Execute and return a single column as an array
240
+ */
241
+ async toArray(column: string): Promise<any[]> {
242
+ return (await this.result()).col(column);
243
+ }
244
+
245
+ /**
246
+ * Execute and return distinct values from a column
247
+ */
248
+ async toDistinct(column: string): Promise<any[]> {
249
+ return (await this.result()).distinct(column);
250
+ }
251
+
252
+ /**
253
+ * Execute and push results into a jux component.
254
+ *
255
+ * Supported targets:
256
+ * - Table: sets columns + rows
257
+ * - Dropdown/Select: sets options from labelKey/valueKey
258
+ * - List: sets items
259
+ * - DataFrameComponent: loads data
260
+ * - Any object with a .rows() or .options() or .items() method
261
+ */
262
+ async into(target: any, options: IntoOptions = {}): Promise<DataFrame> {
263
+ const df = await this.result();
264
+ const rows = df.toRows();
265
+
266
+ // Table-like: has .columns() and .rows()
267
+ if (typeof target.columns === 'function' && typeof target.rows === 'function') {
268
+ const columnDefs = (options.columns ?? df.columns).map((col: string) => {
269
+ const label = options.columnLabels?.[col] ?? col;
270
+ return { key: col, label };
271
+ });
272
+ target.columns(columnDefs).rows(rows);
273
+ return df;
274
+ }
275
+
276
+ // DataFrameComponent: has .fromData()
277
+ if (typeof target.fromData === 'function') {
278
+ target.fromData(rows);
279
+ return df;
280
+ }
281
+
282
+ // Dropdown/Select-like: has .options()
283
+ if (typeof target.options === 'function') {
284
+ const labelKey = options.labelKey ?? df.columns[0];
285
+ const valueKey = options.valueKey ?? (df.columns[1] ?? df.columns[0]);
286
+ const opts = rows.map(row => ({
287
+ label: String(row[labelKey] ?? ''),
288
+ value: row[valueKey]
289
+ }));
290
+ target.options(opts);
291
+ return df;
292
+ }
293
+
294
+ // List-like: has .items()
295
+ if (typeof target.items === 'function') {
296
+ const key = options.labelKey ?? df.columns[0];
297
+ target.items(rows.map(row => String(row[key] ?? '')));
298
+ return df;
299
+ }
300
+
301
+ throw new Error(`Target does not have a recognized API (.columns/.rows, .fromData, .options, or .items)`);
302
+ }
303
+
304
+ /**
305
+ * Execute and persist to IndexedDB
306
+ */
307
+ async store(key: string, metadata?: Record<string, any>): Promise<DataFrame> {
308
+ const df = await this.result();
309
+ await this._driver.store(key, df, metadata);
310
+ return df;
311
+ }
312
+
313
+ /* ═══════════════════════════════════════════════════
314
+ * PRIVATE
315
+ * ═══════════════════════════════════════════════════ */
316
+
317
+ private async _loadFromUrl(url: string, options: FetchOptions): Promise<DataFrame> {
318
+ const mimeType = options.mimeType ?? this._detectMimeType(url);
319
+
320
+ if (mimeType === 'json') {
321
+ const response = await fetch(url);
322
+ if (!response.ok) throw new Error(`Fetch failed: ${response.status} ${response.statusText}`);
323
+ const json = await response.json();
324
+ return new DataFrame(json);
325
+ }
326
+
327
+ // CSV/TSV — delegate to driver
328
+ return this._driver.fetch(url, {
329
+ autoDetectDelimiter: !options.delimiter,
330
+ delimiter: options.delimiter,
331
+ hasHeader: options.hasHeader ?? true,
332
+ headerRow: options.headerRow
333
+ });
334
+ }
335
+
336
+ private async _loadFromFile(file: File, options: FetchOptions): Promise<DataFrame> {
337
+ const isExcel = /\.(xlsx?|xls)$/i.test(file.name);
338
+
339
+ if (isExcel) {
340
+ const sheets = await this._driver.streamFileMultiSheet(file, {
341
+ headerRow: options.headerRow
342
+ });
343
+ const sheetNames = Object.keys(sheets);
344
+ if (sheetNames.length === 0) throw new Error('No data found in file');
345
+ return sheets[sheetNames[0]];
346
+ }
347
+
348
+ const text = await file.text();
349
+ return this._driver.parseCSV(text, {
350
+ autoDetectDelimiter: !options.delimiter,
351
+ delimiter: options.delimiter,
352
+ hasHeader: options.hasHeader ?? true,
353
+ headerRow: options.headerRow
354
+ });
355
+ }
356
+
357
+ private _detectMimeType(url: string): 'csv' | 'tsv' | 'json' | 'auto' {
358
+ const lower = url.toLowerCase().split('?')[0];
359
+ if (lower.endsWith('.json')) return 'json';
360
+ if (lower.endsWith('.tsv')) return 'tsv';
361
+ if (lower.endsWith('.csv')) return 'csv';
362
+ return 'auto';
363
+ }
364
+ }
365
+
366
+ export interface IntoOptions {
367
+ /** Override which columns to use (for Table targets) */
368
+ columns?: string[];
369
+ /** Column name → display label mapping */
370
+ columnLabels?: Record<string, string>;
371
+ /** Key for option labels (Dropdown/Select/List targets) */
372
+ labelKey?: string;
373
+ /** Key for option values (Dropdown/Select targets) */
374
+ valueKey?: string;
375
+ }
376
+
377
+ /**
378
+ * Factory function — the primary entry point
379
+ *
380
+ * Usage:
381
+ * import { data } from 'juxscript';
382
+ *
383
+ * const df = await data().fromStorage('patients').select('name', 'dob').result();
384
+ * await data().fromUrl(presignedUrl).filter(r => r.status === 'active').into(myTable);
385
+ */
386
+ export function data(options: PipelineOptions = {}): DataPipeline {
387
+ return new DataPipeline(options);
388
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "juxscript",
3
- "version": "1.1.232",
3
+ "version": "1.1.234",
4
4
  "type": "module",
5
5
  "description": "A JavaScript UX authorship platform",
6
6
  "main": "index.js",