@web-ts-toolkit/json-frame 0.35.0 → 0.37.0

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.
Files changed (4) hide show
  1. package/README.md +300 -1
  2. package/index.js +2 -22
  3. package/index.mjs +2 -22
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -1,3 +1,302 @@
1
1
  # `@web-ts-toolkit/json-frame`
2
2
 
3
- PLACEHOLDER
3
+ Normalize pandas `DataFrame.to_json()` payloads into one immutable, column-major `DataFrame` API for TypeScript.
4
+
5
+ `@web-ts-toolkit/json-frame` accepts a JSON string or parsed JSON value, ingests all six pandas DataFrame JSON orients, keeps row order and supported index labels intact, and exports back to every supported orient without adding runtime dependencies.
6
+
7
+ ## Installation
8
+
9
+ ```sh
10
+ pnpm add @web-ts-toolkit/json-frame
11
+ ```
12
+
13
+ Canonical import:
14
+
15
+ ```ts
16
+ import { fromOrient } from '@web-ts-toolkit/json-frame';
17
+ ```
18
+
19
+ The package exports named values and types from the package root. There is no default export and no supported deep import path.
20
+
21
+ ## Quick Start
22
+
23
+ ```ts
24
+ import { fromOrient } from '@web-ts-toolkit/json-frame';
25
+
26
+ type WeatherRow = {
27
+ city: string;
28
+ temp: number;
29
+ };
30
+
31
+ const frame = fromOrient<WeatherRow>('[{"city":"Paris","temp":21},{"city":"Rome","temp":30}]');
32
+ const hottest = frame.sort((left, right) => right.temp - left.temp).row(0);
33
+ const split = frame.toSplit();
34
+
35
+ void [hottest, split];
36
+ ```
37
+
38
+ `fromOrient(input, options?)` accepts:
39
+
40
+ - a JSON string produced by pandas `DataFrame.to_json()`
41
+ - a parsed JSON value with the same shape
42
+
43
+ The returned `DataFrame` is immutable. `filter`, `sort`, `select`, `rename`, and `resetIndex` all return new frames.
44
+
45
+ ## Supported Orients
46
+
47
+ All six pandas DataFrame JSON orients are supported.
48
+
49
+ ```ts
50
+ import { fromOrient, type TablePayload } from '@web-ts-toolkit/json-frame';
51
+
52
+ const recordsFrame = fromOrient([{ city: 'Paris', temp: 21 }], { orient: 'records' });
53
+
54
+ const indexFrame = fromOrient(
55
+ {
56
+ row_1: { city: 'Paris', temp: 21 },
57
+ },
58
+ { orient: 'index' },
59
+ );
60
+
61
+ const columnsFrame = fromOrient(
62
+ {
63
+ city: { row_1: 'Paris' },
64
+ temp: { row_1: 21 },
65
+ },
66
+ { orient: 'columns' },
67
+ );
68
+
69
+ const valuesFrame = fromOrient([['Paris', 21]], {
70
+ orient: 'values',
71
+ columns: ['city', 'temp'],
72
+ });
73
+
74
+ const splitFrame = fromOrient(
75
+ {
76
+ columns: ['city', 'temp'],
77
+ index: ['row_1'],
78
+ data: [['Paris', 21]],
79
+ },
80
+ { orient: 'split' },
81
+ );
82
+
83
+ const tablePayload = {
84
+ schema: {
85
+ fields: [
86
+ { name: 'row_id', type: 'string' },
87
+ { name: 'city', type: 'string' },
88
+ { name: 'temp', type: 'integer' },
89
+ ],
90
+ primaryKey: ['row_id'],
91
+ pandas_version: '3.0.3',
92
+ },
93
+ data: [{ row_id: 'row_1', city: 'Paris', temp: 21 }],
94
+ } satisfies TablePayload;
95
+
96
+ const tableFrame = fromOrient(tablePayload, { orient: 'table' });
97
+
98
+ void [recordsFrame, indexFrame, columnsFrame, valuesFrame, splitFrame, tableFrame];
99
+ ```
100
+
101
+ Orient notes:
102
+
103
+ - `records` and `values` do not carry source index labels. They receive a synthetic numeric index `0..n-1`.
104
+ - `index`, `columns`, `split`, and `table` preserve supported source index labels.
105
+ - `values` always requires `options.columns`, including empty input.
106
+ - `index` and `columns` are distinct supported payloads, but auto-detection cannot safely distinguish them from nested-object structure alone.
107
+
108
+ ## Auto Detection And Options
109
+
110
+ `options.orient` defaults to `auto`.
111
+
112
+ Auto-detection recognizes only structurally unambiguous payloads:
113
+
114
+ - `table`
115
+ - `split`
116
+ - non-empty `records`
117
+ - non-empty `values`
118
+
119
+ Pass an explicit orient for:
120
+
121
+ - any `index` payload
122
+ - any `columns` payload
123
+ - empty arrays
124
+ - empty objects
125
+ - empty `values` payloads
126
+
127
+ Other `fromOrient` options:
128
+
129
+ - `columns`: required for `orient: 'values'`
130
+ - `columnTypes`: explicit logical types for non-table inputs
131
+ - `packThreshold`: minimum length for internal numeric typed-array packing; `0` disables packing
132
+
133
+ ```ts
134
+ import { AmbiguousOrientError, JsonFrameOptionError, fromOrient } from '@web-ts-toolkit/json-frame';
135
+
136
+ let candidates: readonly string[] = [];
137
+ let option = '';
138
+
139
+ try {
140
+ fromOrient({ row_1: { city: 'Paris', temp: 21 } });
141
+ } catch (error) {
142
+ if (error instanceof AmbiguousOrientError) {
143
+ candidates = error.candidates;
144
+ }
145
+ }
146
+
147
+ try {
148
+ fromOrient([['Paris', 21]], { orient: 'values' });
149
+ } catch (error) {
150
+ if (error instanceof JsonFrameOptionError) {
151
+ option = error.option;
152
+ }
153
+ }
154
+
155
+ void [candidates, option];
156
+ ```
157
+
158
+ ## Transforms And Exporters
159
+
160
+ The `DataFrame` API is intentionally small and predictable.
161
+
162
+ Read access:
163
+
164
+ - `columns`
165
+ - `index`
166
+ - `columnInfo`
167
+ - `length`
168
+ - `row(position)`
169
+ - `rows()`
170
+
171
+ Transforms:
172
+
173
+ - `filter(predicate)`
174
+ - `sort(compare)`
175
+ - `select(...columns)`
176
+ - `rename(mapping)`
177
+ - `resetIndex()`
178
+
179
+ Exporters:
180
+
181
+ - `toRecords()`
182
+ - `toIndex()`
183
+ - `toColumns()`
184
+ - `toValues()`
185
+ - `toSplit()`
186
+ - `toTable(options?)`
187
+ - `toJSONString(orient, options?)`
188
+
189
+ ```ts
190
+ import { fromOrient } from '@web-ts-toolkit/json-frame';
191
+
192
+ type WeatherRow = {
193
+ city: string;
194
+ temp: number | null;
195
+ coastal: boolean;
196
+ };
197
+
198
+ const frame = fromOrient<WeatherRow>(
199
+ [
200
+ { city: 'Paris', temp: 21.5, coastal: false },
201
+ { city: 'Tokyo', temp: 27.1, coastal: true },
202
+ { city: 'Oslo', temp: null, coastal: true },
203
+ ],
204
+ {
205
+ orient: 'records',
206
+ columnTypes: {
207
+ temp: 'float',
208
+ coastal: 'boolean',
209
+ },
210
+ packThreshold: 0,
211
+ },
212
+ );
213
+
214
+ const transformed = frame
215
+ .filter((row) => row.temp !== null)
216
+ .sort((left, right) => (right.temp ?? -1) - (left.temp ?? -1))
217
+ .rename({ temp: 'celsius' })
218
+ .select('city', 'celsius')
219
+ .resetIndex();
220
+
221
+ const table = transformed.toTable();
222
+ const json = transformed.toJSONString('records');
223
+
224
+ void [table, json];
225
+ ```
226
+
227
+ Transform behavior:
228
+
229
+ - row order is preserved unless you call `sort`
230
+ - `sort` is stable
231
+ - filtering and sorting preserve index labels
232
+ - `resetIndex()` is the only transform that replaces the current index with `0..n-1`
233
+ - `rename` ignores keys that are not current columns and rejects duplicate results
234
+ - exporters always return fresh JSON-compatible containers
235
+
236
+ ## Logical Types And Packing
237
+
238
+ `columnInfo` exposes logical type metadata for each column.
239
+
240
+ Supported logical types:
241
+
242
+ - `integer`
243
+ - `float`
244
+ - `string`
245
+ - `boolean`
246
+ - `datetime`
247
+ - `categorical`
248
+ - `mixed`
249
+ - `unknown`
250
+
251
+ Type rules:
252
+
253
+ - Table Schema metadata is authoritative when `orient: 'table'` is used.
254
+ - Other orients infer the narrowest logical type by scanning the full column.
255
+ - `null` marks a column as nullable without forcing an otherwise numeric column to become `mixed`.
256
+ - ISO-looking strings are preserved as strings unless table metadata or explicit `columnTypes` says otherwise.
257
+
258
+ Packing rules:
259
+
260
+ - typed-array packing is an internal optimization only
261
+ - packed and unpacked frames export the same JSON values
262
+ - packing never changes logical type metadata
263
+ - only non-null numeric columns at or above `packThreshold` are eligible
264
+
265
+ ## Round Trips And Index Provenance
266
+
267
+ Round trips are semantic rather than byte-for-byte.
268
+
269
+ - column order, row order, and cell JSON values are preserved
270
+ - label-bearing orients preserve supported source index labels
271
+ - `records` and `values` use a synthetic numeric index because the wire format does not carry labels
272
+ - `toRecords()` and `toValues()` never invent an `_index` column
273
+ - `toIndex()` and `toColumns()` stringify index labels as JSON object keys and throw if distinct labels would collide after stringification
274
+ - `toTable()` emits valid Table Schema and omits a synthetic index from table output
275
+
276
+ ## Errors
277
+
278
+ Structured runtime errors are exported from the package root.
279
+
280
+ - `JsonFrameParseError`: invalid JSON string input; original `SyntaxError` is available as `cause`
281
+ - `JsonFrameOptionError`: invalid options such as bad `packThreshold` or missing `columns` for `values`
282
+ - `JsonFrameValidationError`: payload shape or JSON-value validation failure
283
+ - `AmbiguousOrientError`: `auto` mode cannot distinguish between multiple valid orients
284
+ - `UnsupportedFeatureError`: supported contract deliberately excludes the requested pandas feature
285
+ - `ExportKeyCollisionError`: object-key exporters would collapse distinct index labels to the same JSON key
286
+
287
+ ## Unsupported Pandas Features In The Initial Release
288
+
289
+ - MultiIndex input or output
290
+ - duplicate column labels
291
+ - non-string column labels
292
+ - pandas Series JSON shapes
293
+ - JSON Lines, compression, file I/O, or streaming input
294
+ - arbitrary pandas extension dtype reconstruction beyond supported Table Schema metadata
295
+ - deep freezing of nested JSON object values
296
+
297
+ ## Runtime Requirements
298
+
299
+ - package source and published runtime are isomorphic
300
+ - no runtime dependencies
301
+ - no peer dependencies
302
+ - published entrypoints: CJS, ESM, `.d.ts`, and `.d.mts`
package/index.js CHANGED
@@ -199,34 +199,14 @@ var getIndexFieldTemplate = (state) => {
199
199
  if (state.tableSchema === void 0 || state.tableIndexField === void 0) {
200
200
  return void 0;
201
201
  }
202
- if (state.tableSchema.fields.length === state.columns.length + 1) {
203
- return state.tableSchema.fields[0];
204
- }
205
202
  return state.tableSchema.fields.find((field) => field.name === state.tableIndexField);
206
203
  };
207
204
  var getDataFieldTemplates = (state) => {
208
205
  if (state.tableSchema === void 0) {
209
206
  return [];
210
207
  }
211
- if (state.tableIndexField === void 0) {
212
- return state.tableSchema.fields.slice(0, state.columns.length);
213
- }
214
- if (state.tableSchema.fields.length >= state.columns.length + 1) {
215
- return state.tableSchema.fields.slice(1, state.columns.length + 1);
216
- }
217
- let skippedIndexField = false;
218
- const templates = [];
219
- for (const field of state.tableSchema.fields) {
220
- if (!skippedIndexField && field.name === state.tableIndexField) {
221
- skippedIndexField = true;
222
- continue;
223
- }
224
- templates.push(field);
225
- if (templates.length === state.columns.length) {
226
- break;
227
- }
228
- }
229
- return templates;
208
+ const fieldsByName = new Map(state.tableSchema.fields.map((field) => [field.name, field]));
209
+ return state.columns.map((column) => fieldsByName.get(column)).filter((field) => field !== void 0);
230
210
  };
231
211
  var resolveIndexFieldName = (state, options) => {
232
212
  if (options !== void 0 && (Array.isArray(options) || typeof options !== "object" || options === null)) {
package/index.mjs CHANGED
@@ -167,34 +167,14 @@ var getIndexFieldTemplate = (state) => {
167
167
  if (state.tableSchema === void 0 || state.tableIndexField === void 0) {
168
168
  return void 0;
169
169
  }
170
- if (state.tableSchema.fields.length === state.columns.length + 1) {
171
- return state.tableSchema.fields[0];
172
- }
173
170
  return state.tableSchema.fields.find((field) => field.name === state.tableIndexField);
174
171
  };
175
172
  var getDataFieldTemplates = (state) => {
176
173
  if (state.tableSchema === void 0) {
177
174
  return [];
178
175
  }
179
- if (state.tableIndexField === void 0) {
180
- return state.tableSchema.fields.slice(0, state.columns.length);
181
- }
182
- if (state.tableSchema.fields.length >= state.columns.length + 1) {
183
- return state.tableSchema.fields.slice(1, state.columns.length + 1);
184
- }
185
- let skippedIndexField = false;
186
- const templates = [];
187
- for (const field of state.tableSchema.fields) {
188
- if (!skippedIndexField && field.name === state.tableIndexField) {
189
- skippedIndexField = true;
190
- continue;
191
- }
192
- templates.push(field);
193
- if (templates.length === state.columns.length) {
194
- break;
195
- }
196
- }
197
- return templates;
176
+ const fieldsByName = new Map(state.tableSchema.fields.map((field) => [field.name, field]));
177
+ return state.columns.map((column) => fieldsByName.get(column)).filter((field) => field !== void 0);
198
178
  };
199
179
  var resolveIndexFieldName = (state, options) => {
200
180
  if (options !== void 0 && (Array.isArray(options) || typeof options !== "object" || options === null)) {
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@web-ts-toolkit/json-frame",
3
3
  "description": "Normalize and transform any tabular JSON shape (records, columns, split, values, table, index) into a single column-major DataFrame view",
4
4
  "homepage": "https://web-ts-toolkit.pages.dev/docs/packages/json-frame",
5
- "version": "0.35.0",
5
+ "version": "0.37.0",
6
6
  "sideEffects": false,
7
7
  "keywords": [
8
8
  "dataframe",