@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.
- package/README.md +300 -1
- package/index.js +2 -22
- package/index.mjs +2 -22
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,3 +1,302 @@
|
|
|
1
1
|
# `@web-ts-toolkit/json-frame`
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
212
|
-
|
|
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
|
-
|
|
180
|
-
|
|
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.
|
|
5
|
+
"version": "0.37.0",
|
|
6
6
|
"sideEffects": false,
|
|
7
7
|
"keywords": [
|
|
8
8
|
"dataframe",
|