@tidyjs/tidy 2.6.0 → 2.6.1

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,400 @@
1
+ # Grouping API Reference
2
+
3
+ Detailed reference for `groupBy` and its export modes.
4
+
5
+ ```js
6
+ import { tidy, groupBy, summarize, sum, mean, n } from '@tidyjs/tidy';
7
+ ```
8
+
9
+ ---
10
+
11
+ <!-- keywords: groupBy, group, nest, aggregate, split-apply-combine, grouped data -->
12
+ ## groupBy
13
+
14
+ Groups data by one or more keys, runs tidy functions on each group, and returns results either as a flat array (default) or in a structured export format.
15
+
16
+ **Signature:**
17
+ ```ts
18
+ groupBy(
19
+ groupKeys: string | (d: T) => any | Array<string | (d: T) => any>,
20
+ fns: TidyFn | TidyFn[],
21
+ options?: GroupByOptions | ExportShortcut
22
+ )
23
+ ```
24
+ **Goes inside:** `tidy()` pipeline
25
+
26
+ ### Parameters
27
+
28
+ - **groupKeys** -- what to group by
29
+ - `string` -- a property name: `'category'`
30
+ - `(d) => any` -- an accessor function: `(d) => d.date.getFullYear()`
31
+ - `Array<string | (d) => any>` -- multiple keys: `['category', 'region']`
32
+ - Grouping uses strict equality (`===`). For non-primitive keys, return a primitive from an accessor function.
33
+ - **fns** -- tidy functions to run on each group's subset
34
+ - A single function: `summarize({ total: sum('value') })`
35
+ - An array of functions: `[filter((d) => d.active), summarize({ n: n() })]`
36
+ - **options?** -- controls output shape
37
+ - Omitted or `undefined` -- returns a flat array with group keys merged back in (default)
38
+ - `{ addGroupKeys: false }` -- flat array without merging group keys into results
39
+ - An export shortcut (`groupBy.object()`, `groupBy.entries()`, etc.) -- returns structured output; makes the function a `TidyGroupExportFn` (must be last in the pipeline)
40
+
41
+ ### Behavior
42
+
43
+ - Group keys are automatically merged back into each result item (disable with `addGroupKeys: false`).
44
+ - Without an export option, output is a flat `T[]` -- the results from all groups concatenated.
45
+ - With an export option, the return type changes (object, entries, Map, etc.) and the `groupBy` call must be the **last** step in the `tidy()` pipeline.
46
+
47
+ ### Example: Default (flat array)
48
+
49
+ ```js
50
+ const data = [
51
+ { str: 'a', ing: 'x', value: 1 },
52
+ { str: 'b', ing: 'x', value: 100 },
53
+ { str: 'a', ing: 'y', value: 2 },
54
+ { str: 'b', ing: 'y', value: 300 },
55
+ ];
56
+
57
+ tidy(data, groupBy('str', [summarize({ total: sum('value') })]))
58
+ // output:
59
+ // [
60
+ // { str: 'a', total: 3 },
61
+ // { str: 'b', total: 400 }
62
+ // ]
63
+ ```
64
+
65
+ ### Example: Multi-key grouping
66
+
67
+ ```js
68
+ tidy(data, groupBy(['str', 'ing'], [summarize({ total: sum('value') })]))
69
+ // output:
70
+ // [
71
+ // { str: 'a', ing: 'x', total: 1 },
72
+ // { str: 'a', ing: 'y', total: 2 },
73
+ // { str: 'b', ing: 'x', total: 100 },
74
+ // { str: 'b', ing: 'y', total: 300 }
75
+ // ]
76
+ ```
77
+
78
+ ### Example: Accessor function as key
79
+
80
+ ```js
81
+ tidy(
82
+ data,
83
+ groupBy(
84
+ (d) => (d.value >= 100 ? 'high' : 'low'),
85
+ [summarize({ n: n() })]
86
+ )
87
+ )
88
+ // output:
89
+ // [{ n: 2 }, { n: 2 }]
90
+ // Note: accessor-function keys are not merged back (no property name to use).
91
+ ```
92
+
93
+ ---
94
+
95
+ <!-- keywords: export, entries, nested, key-value pairs -->
96
+ ## groupBy.entries()
97
+
98
+ Returns `[[key, values], ...]` -- an array of `[key, nestedEntriesOrLeafArray]` tuples.
99
+
100
+ **Signature:** `groupBy.entries(options?: ExportOptions)`
101
+ **Goes inside:** 3rd argument of `groupBy()`
102
+
103
+ ### Example
104
+
105
+ ```js
106
+ tidy(
107
+ data,
108
+ groupBy('str', [summarize({ total: sum('value') })], groupBy.entries())
109
+ )
110
+ // output:
111
+ // [
112
+ // ['a', [{ str: 'a', total: 3 }]],
113
+ // ['b', [{ str: 'b', total: 400 }]]
114
+ // ]
115
+ ```
116
+
117
+ With `single: true` (unwraps single-item leaf arrays):
118
+
119
+ ```js
120
+ tidy(
121
+ data,
122
+ groupBy('str', [summarize({ total: sum('value') })], groupBy.entries({ single: true }))
123
+ )
124
+ // output:
125
+ // [
126
+ // ['a', { str: 'a', total: 3 }],
127
+ // ['b', { str: 'b', total: 400 }]
128
+ // ]
129
+ ```
130
+
131
+ ---
132
+
133
+ <!-- keywords: export, entriesObject, key values object -->
134
+ ## groupBy.entriesObject()
135
+
136
+ Returns `[{ key, values }, ...]` -- like `entries()` but as objects instead of tuples.
137
+
138
+ **Signature:** `groupBy.entriesObject(options?: ExportOptions)`
139
+ **Goes inside:** 3rd argument of `groupBy()`
140
+
141
+ ### Example
142
+
143
+ ```js
144
+ tidy(
145
+ data,
146
+ groupBy('str', [summarize({ total: sum('value') })], groupBy.entriesObject())
147
+ )
148
+ // output:
149
+ // [
150
+ // { key: 'a', values: [{ str: 'a', total: 3 }] },
151
+ // { key: 'b', values: [{ str: 'b', total: 400 }] }
152
+ // ]
153
+ ```
154
+
155
+ ---
156
+
157
+ <!-- keywords: export, object, dictionary, lookup, key-value -->
158
+ ## groupBy.object()
159
+
160
+ Returns `{ key: values, ... }` -- a plain object keyed by group values.
161
+
162
+ **Signature:** `groupBy.object(options?: ExportOptions)`
163
+ **Goes inside:** 3rd argument of `groupBy()`
164
+
165
+ ### Example
166
+
167
+ ```js
168
+ tidy(
169
+ data,
170
+ groupBy('str', [summarize({ total: sum('value') })], groupBy.object())
171
+ )
172
+ // output:
173
+ // {
174
+ // a: [{ str: 'a', total: 3 }],
175
+ // b: [{ str: 'b', total: 400 }]
176
+ // }
177
+ ```
178
+
179
+ Multi-key produces nested objects:
180
+
181
+ ```js
182
+ tidy(
183
+ data,
184
+ groupBy(
185
+ ['str', 'ing'],
186
+ [summarize({ total: sum('value') })],
187
+ groupBy.object({ single: true })
188
+ )
189
+ )
190
+ // output:
191
+ // {
192
+ // a: {
193
+ // x: { str: 'a', ing: 'x', total: 1 },
194
+ // y: { str: 'a', ing: 'y', total: 2 }
195
+ // },
196
+ // b: {
197
+ // x: { str: 'b', ing: 'x', total: 100 },
198
+ // y: { str: 'b', ing: 'y', total: 300 }
199
+ // }
200
+ // }
201
+ ```
202
+
203
+ ---
204
+
205
+ <!-- keywords: export, map, ES Map -->
206
+ ## groupBy.map()
207
+
208
+ Returns an ES `Map` where keys are group values (not tuples) and values are nested Maps or leaf arrays.
209
+
210
+ **Signature:** `groupBy.map(options?: ExportOptions)`
211
+ **Goes inside:** 3rd argument of `groupBy()`
212
+
213
+ ### Example
214
+
215
+ ```js
216
+ tidy(
217
+ data,
218
+ groupBy('str', [summarize({ total: sum('value') })], groupBy.map())
219
+ )
220
+ // output:
221
+ // new Map([
222
+ // ['a', [{ str: 'a', total: 3 }]],
223
+ // ['b', [{ str: 'b', total: 400 }]]
224
+ // ])
225
+ ```
226
+
227
+ ---
228
+
229
+ <!-- keywords: export, grouped, internal, Grouped Map, tuple keys -->
230
+ ## groupBy.grouped()
231
+
232
+ Returns the raw internal `Grouped<T>` structure -- a `Map` where keys are `[keyName, keyValue]` tuples.
233
+
234
+ **Signature:** `groupBy.grouped(options?: ExportOptions)`
235
+ **Goes inside:** 3rd argument of `groupBy()`
236
+
237
+ ### Example
238
+
239
+ ```js
240
+ tidy(
241
+ data,
242
+ groupBy('str', [summarize({ total: sum('value') })], groupBy.grouped())
243
+ )
244
+ // output:
245
+ // new Map([
246
+ // [['str', 'a'], [{ str: 'a', total: 3 }]],
247
+ // [['str', 'b'], [{ str: 'b', total: 400 }]]
248
+ // ])
249
+ ```
250
+
251
+ ---
252
+
253
+ <!-- keywords: export, keys, group keys only -->
254
+ ## groupBy.keys()
255
+
256
+ Returns just the group key values, no data.
257
+
258
+ **Signature:** `groupBy.keys(options?: ExportOptions)`
259
+ **Goes inside:** 3rd argument of `groupBy()`
260
+
261
+ ### Example
262
+
263
+ ```js
264
+ tidy(
265
+ data,
266
+ groupBy('str', [summarize({ total: sum('value') })], groupBy.keys())
267
+ )
268
+ // output:
269
+ // ['a', 'b']
270
+
271
+ // Multi-key:
272
+ tidy(
273
+ data,
274
+ groupBy(['str', 'ing'], [summarize({ total: sum('value') })], groupBy.keys())
275
+ )
276
+ // output:
277
+ // [['a', ['x', 'y']], ['b', ['x', 'y']]]
278
+ ```
279
+
280
+ ---
281
+
282
+ <!-- keywords: export, values, grouped values only -->
283
+ ## groupBy.values()
284
+
285
+ Returns just the grouped value arrays, no keys.
286
+
287
+ **Signature:** `groupBy.values(options?: ExportOptions)`
288
+ **Goes inside:** 3rd argument of `groupBy()`
289
+
290
+ ### Example
291
+
292
+ ```js
293
+ tidy(
294
+ data,
295
+ groupBy('str', [summarize({ total: sum('value') })], groupBy.values())
296
+ )
297
+ // output:
298
+ // [
299
+ // [{ str: 'a', total: 3 }],
300
+ // [{ str: 'b', total: 400 }]
301
+ // ]
302
+ ```
303
+
304
+ ---
305
+
306
+ <!-- keywords: export, levels, per-level, mixed export, custom levels -->
307
+ ## groupBy.levels()
308
+
309
+ Per-level export control for multi-key grouping. Each level can use a different export format. The last level specified repeats for remaining levels.
310
+
311
+ **Signature:** `groupBy.levels(options: ExportOptions & { levels: LevelSpec[] })`
312
+ **Goes inside:** 3rd argument of `groupBy()`
313
+
314
+ ### Parameters (in the options object)
315
+
316
+ - **levels** (required) -- array of export type strings, one per grouping level:
317
+ `'entries'` | `'entries-object'` | `'object'` | `'map'` | `'keys'` | `'values'` | `LevelSpec`
318
+ - Plus all common export options (`single`, `flat`, `mapLeaf`, etc.)
319
+
320
+ ### Example
321
+
322
+ ```js
323
+ tidy(
324
+ data,
325
+ groupBy(
326
+ ['str', 'ing'],
327
+ [summarize({ total: sum('value') })],
328
+ groupBy.levels({ levels: ['entries-object', 'object'], single: true })
329
+ )
330
+ )
331
+ // output:
332
+ // [
333
+ // {
334
+ // key: 'a',
335
+ // values: {
336
+ // x: { str: 'a', ing: 'x', total: 1 },
337
+ // y: { str: 'a', ing: 'y', total: 2 }
338
+ // }
339
+ // },
340
+ // {
341
+ // key: 'b',
342
+ // values: {
343
+ // x: { str: 'b', ing: 'x', total: 100 },
344
+ // y: { str: 'b', ing: 'y', total: 300 }
345
+ // }
346
+ // }
347
+ // ]
348
+ ```
349
+
350
+ ---
351
+
352
+ <!-- keywords: export options, single, flat, compositeKey, mapLeaf, mapLeaves, mapEntry, addGroupKeys -->
353
+ ## Common Export Options
354
+
355
+ All export shortcut helpers (`groupBy.entries()`, `groupBy.object()`, etc.) accept an options object with these fields:
356
+
357
+ | Option | Type | Default | Description |
358
+ |---|---|---|---|
359
+ | `single` | `boolean` | `false` | Unwrap single-item leaf arrays to just the item. Typical after `summarize`. |
360
+ | `flat` | `boolean` | `false` | Flatten nested groups to a single level (multi-key grouping becomes one level). |
361
+ | `compositeKey` | `(keys: any[]) => string` | `keys.join('/')` | Custom function to create flat keys. Only used when `flat: true`. |
362
+ | `mapLeaf` | `(value: T) => any` | identity | Transform each individual leaf item during export. |
363
+ | `mapLeaves` | `(values: T[]) => any` | identity | Transform each leaf array (similar to d3 `rollup`). |
364
+ | `mapEntry` | `(entry: [key, values], level) => any` | identity | Transform each entry (for `entries` export only). |
365
+ | `addGroupKeys` | `boolean` | `true` | Set to `false` to prevent merging group keys back into result objects. |
366
+
367
+ ### Example: flat with compositeKey
368
+
369
+ ```js
370
+ tidy(
371
+ data,
372
+ groupBy(
373
+ ['str', 'ing'],
374
+ [summarize({ total: sum('value') })],
375
+ groupBy.object({ flat: true, single: true, compositeKey: (keys) => keys.join('-') })
376
+ )
377
+ )
378
+ // output:
379
+ // {
380
+ // 'a-x': { str: 'a', ing: 'x', total: 1 },
381
+ // 'a-y': { str: 'a', ing: 'y', total: 2 },
382
+ // 'b-x': { str: 'b', ing: 'x', total: 100 },
383
+ // 'b-y': { str: 'b', ing: 'y', total: 300 }
384
+ // }
385
+ ```
386
+
387
+ ### Example: mapLeaves
388
+
389
+ ```js
390
+ tidy(
391
+ data,
392
+ groupBy(
393
+ 'str',
394
+ [summarize({ total: sum('value') })],
395
+ groupBy.entries({ mapLeaves: (leaves) => leaves.length })
396
+ )
397
+ )
398
+ // output:
399
+ // [['a', 1], ['b', 1]]
400
+ ```
@@ -0,0 +1,118 @@
1
+ # Joins
2
+
3
+ Join two collections by matching keys.
4
+
5
+ ```js
6
+ import { tidy, innerJoin, leftJoin, fullJoin } from '@tidyjs/tidy';
7
+ ```
8
+
9
+ ---
10
+
11
+ <!-- keywords: inner join, merge, combine, matching rows -->
12
+ ## innerJoin
13
+
14
+ Keep only rows that have a match in both collections.
15
+
16
+ **Signature:** `innerJoin<T, JoinT>(itemsToJoin: JoinT[], options?: { by?: string | string[] | Record<string, string> })`
17
+ **Goes inside:** `tidy(data, innerJoin(...))`
18
+
19
+ ### Parameters
20
+ - **itemsToJoin** `JoinT[]` -- the right-side array to join against.
21
+ - **options.by** `string | string[] | { [rightKey]: leftKey }` -- how to match rows. A single key name, array of shared key names, or an object mapping right-side keys to left-side keys. If omitted, auto-detects shared keys from the first elements of each array.
22
+
23
+ ### Example
24
+ ```js
25
+ const data = [
26
+ { id: 1, value: 10 },
27
+ { id: 2, value: 20 },
28
+ ];
29
+ const lookup = [
30
+ { id: 1, label: 'a' },
31
+ { id: 3, label: 'c' },
32
+ ];
33
+
34
+ tidy(data, innerJoin(lookup, { by: 'id' }));
35
+ // output: [{ id: 1, value: 10, label: 'a' }]
36
+ // row id=2 dropped (no match), row id=3 dropped (not in left)
37
+ ```
38
+
39
+ ---
40
+
41
+ <!-- keywords: left join, merge, keep all left, nullable right -->
42
+ ## leftJoin
43
+
44
+ Keep all left-side rows. Adds right-side columns where a match exists; fills with `undefined` where no match is found.
45
+
46
+ **Signature:** `leftJoin<T, JoinT>(itemsToJoin: JoinT[], options?: { by?: string | string[] | Record<string, string> })`
47
+ **Goes inside:** `tidy(data, leftJoin(...))`
48
+
49
+ ### Parameters
50
+ - **itemsToJoin** `JoinT[]` -- the right-side array to join against.
51
+ - **options.by** -- same as `innerJoin`. If omitted, auto-detects shared keys.
52
+
53
+ ### Example
54
+ ```js
55
+ const data = [
56
+ { id: 1, value: 10 },
57
+ { id: 2, value: 20 },
58
+ ];
59
+ const lookup = [
60
+ { id: 1, label: 'a' },
61
+ ];
62
+
63
+ tidy(data, leftJoin(lookup, { by: 'id' }));
64
+ // output:
65
+ // [
66
+ // { id: 1, value: 10, label: 'a' },
67
+ // { id: 2, value: 20, label: undefined },
68
+ // ]
69
+ ```
70
+
71
+ ---
72
+
73
+ <!-- keywords: full join, outer join, merge, keep all rows -->
74
+ ## fullJoin
75
+
76
+ Keep all rows from both sides. Unmatched columns are filled with `undefined`.
77
+
78
+ **Signature:** `fullJoin<T, JoinT>(itemsToJoin: JoinT[], options?: { by?: string | string[] | Record<string, string> })`
79
+ **Goes inside:** `tidy(data, fullJoin(...))`
80
+
81
+ ### Parameters
82
+ - **itemsToJoin** `JoinT[]` -- the right-side array to join against.
83
+ - **options.by** -- same as `innerJoin`. If omitted, auto-detects shared keys.
84
+
85
+ ### Example
86
+ ```js
87
+ const data = [
88
+ { id: 1, value: 10 },
89
+ { id: 2, value: 20 },
90
+ ];
91
+ const other = [
92
+ { id: 2, label: 'b' },
93
+ { id: 3, label: 'c' },
94
+ ];
95
+
96
+ tidy(data, fullJoin(other, { by: 'id' }));
97
+ // output:
98
+ // [
99
+ // { id: 1, value: 10, label: undefined },
100
+ // { id: 2, value: 20, label: 'b' },
101
+ // { id: undefined, value: undefined, label: 'c' },
102
+ // ]
103
+ // id=3 row added from right side with left columns undefined
104
+ ```
105
+
106
+ ---
107
+
108
+ ## Joining on different key names
109
+
110
+ Use an object `{ rightKey: 'leftKey' }` to join when column names differ:
111
+
112
+ ```js
113
+ const orders = [{ orderId: 1, userId: 100 }];
114
+ const users = [{ uid: 100, name: 'Alice' }];
115
+
116
+ tidy(orders, leftJoin(users, { by: { uid: 'userId' } }));
117
+ // output: [{ orderId: 1, userId: 100, uid: 100, name: 'Alice' }]
118
+ ```