@rokkit/data 1.0.0-next.94
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/LICENSE +21 -0
- package/README.md +50 -0
- package/package.json +56 -0
- package/src/aggregators.js +24 -0
- package/src/constants.js +72 -0
- package/src/convert.js +36 -0
- package/src/dataset.js +442 -0
- package/src/filter.js +57 -0
- package/src/formatter.js +56 -0
- package/src/hierarchy.js +136 -0
- package/src/index.js +6 -0
- package/src/infer.js +263 -0
- package/src/list.js +232 -0
- package/src/metadata.js +106 -0
- package/src/model.js +100 -0
- package/src/parser.js +110 -0
- package/src/renamer.js +98 -0
- package/src/rollup.js +138 -0
- package/src/types.js +208 -0
- package/src/utils.js +31 -0
- package/src/view.js +187 -0
package/src/dataset.js
ADDED
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
import { equals, identity, pick, omit, clone } from 'ramda'
|
|
2
|
+
import { descending } from 'd3-array'
|
|
3
|
+
import { pickAllowedConfig, defaultConfig, includeAll } from './constants'
|
|
4
|
+
import { deriveSortableColumn } from './infer'
|
|
5
|
+
import { groupDataByKeys, fillAlignedData, getAlignGenerator, aggregateData } from './rollup'
|
|
6
|
+
/**
|
|
7
|
+
* Dataset is a collection of data with a set of operations that can be performed on it.
|
|
8
|
+
* @param {Array} data - The data to be stored in the dataset.
|
|
9
|
+
* @param {Object} options - The configuration options for the dataset.
|
|
10
|
+
* @returns {Object} - An object with a set of operations that can be performed on the dataset.
|
|
11
|
+
*/
|
|
12
|
+
// eslint-disable-next-line max-lines-per-function
|
|
13
|
+
export function dataset(data, options = {}) {
|
|
14
|
+
const config = { ...clone(defaultConfig), ...options }
|
|
15
|
+
|
|
16
|
+
const actions = {
|
|
17
|
+
// configuration
|
|
18
|
+
override: (props) => dataset(data, { ...config, ...pickAllowedConfig(props) }),
|
|
19
|
+
where: (condition) => dataset(data, where(config, condition)),
|
|
20
|
+
groupBy: (...fields) => dataset(data, groupBy(config, ...fields)),
|
|
21
|
+
alignBy: (...fields) => dataset(data, alignBy(config, ...fields)),
|
|
22
|
+
using: (template) => dataset(data, usingTemplate(config, template)),
|
|
23
|
+
summarize: (from, fields) => dataset(data, summarize(config, from, fields)),
|
|
24
|
+
// alter keys
|
|
25
|
+
rename: (how) => dataset(renameKeys(data, how)),
|
|
26
|
+
drop: (...fields) => dataset(dropKeys(data, ...fields)),
|
|
27
|
+
// alter rows
|
|
28
|
+
sortBy: (...fields) => dataset(sortDataBy(data, ...fields)),
|
|
29
|
+
delete: () => dataset(deleteRows(data, config.filter || includeAll)),
|
|
30
|
+
update: (value) => dataset(updateRows(data, config.filter || identity, value)),
|
|
31
|
+
fillNA: (value) => dataset(fillNA(data, value)),
|
|
32
|
+
// transform data
|
|
33
|
+
apply: (callback) => dataset(data.map(callback)),
|
|
34
|
+
rollup: () => dataset(rollup(data, config)),
|
|
35
|
+
select: (...cols) => selectKeys(data, config, ...cols),
|
|
36
|
+
// set operations
|
|
37
|
+
union: (other) => dataset(data.concat(other)),
|
|
38
|
+
minus: (other) => dataset(data.filter((d) => !other.find((x) => equals(x, d)))),
|
|
39
|
+
intersect: (other) => dataset(data.filter((d) => other.find((x) => equals(x, d)))),
|
|
40
|
+
// joins
|
|
41
|
+
innerJoin: (other, condition) => dataset(innerJoin(data, other.select(), condition)),
|
|
42
|
+
leftJoin: (other, condition) => dataset(leftJoin(data, other.select(), condition)),
|
|
43
|
+
rightJoin: (other, condition) => dataset(rightJoin(data, other.select(), condition)),
|
|
44
|
+
fullJoin: (other, condition) => dataset(fullJoin(data, other.select(), condition)),
|
|
45
|
+
crossJoin: (other) => dataset(crossJoin(data, other.select())),
|
|
46
|
+
semiJoin: (other, condition) => dataset(semiJoin(data, other.select(), condition)),
|
|
47
|
+
antiJoin: (other, condition) => dataset(antiJoin(data, other.select(), condition)),
|
|
48
|
+
nestedJoin: (other, condition) =>
|
|
49
|
+
dataset(nestedJoin(data, other.select(), condition, config.children))
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return actions
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Adds a filter to the dataset using the provided condition. This filter is applied
|
|
57
|
+
* in subsequent operations like select, delete, and update.
|
|
58
|
+
*
|
|
59
|
+
* @param {Object} config - The configuration object to be updated.
|
|
60
|
+
* @param {Function} condition - The condition function to apply.
|
|
61
|
+
* @returns {Object} - The updated configuration
|
|
62
|
+
*/
|
|
63
|
+
function where(config, condition) {
|
|
64
|
+
config.filter = condition
|
|
65
|
+
return config
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Groups the dataset by the specified columns.
|
|
69
|
+
* @param {Object} config - The configuration object to be updated.
|
|
70
|
+
* @param {Object} actions - The actions object for method chaining.
|
|
71
|
+
* @param {...string} fields - The columns to group by.
|
|
72
|
+
* @returns {Object} - The updated configurations.
|
|
73
|
+
*/
|
|
74
|
+
function groupBy(config, ...fields) {
|
|
75
|
+
config.group_by = fields
|
|
76
|
+
return config
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Aligns the columns of the dataset using the provided fields.
|
|
81
|
+
*
|
|
82
|
+
* @param {Object} config - The configuration object to be updated.
|
|
83
|
+
* @param {...string} fields - The fields to align.
|
|
84
|
+
* @returns {Object} - The updated configuration.
|
|
85
|
+
*/
|
|
86
|
+
function alignBy(config, ...fields) {
|
|
87
|
+
config.align_by = fields
|
|
88
|
+
return config
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Sets the template for adding empty rows in the dataset.
|
|
92
|
+
* @param {Object} config - The configuration object to be updated.
|
|
93
|
+
* @param {Object} template - The template to use for adding empty rows.
|
|
94
|
+
* @returns {Object} - The updated configuration.
|
|
95
|
+
*/
|
|
96
|
+
function usingTemplate(config, template) {
|
|
97
|
+
config.template = template
|
|
98
|
+
return config
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Adds a summary field to the dataset.
|
|
103
|
+
*
|
|
104
|
+
* @param {Object} config - The configuration object to be updated.
|
|
105
|
+
* @param {string|Array<string>|Function} from - The field or function to fetch data for summary
|
|
106
|
+
* @param {string|Object<string:Function>} using - The target field & formula to use for summarizing.
|
|
107
|
+
* @returns {Object} - The updated configuration.
|
|
108
|
+
*/
|
|
109
|
+
function summarize(config, from, using) {
|
|
110
|
+
const mapper = typeof from === 'function' ? from : Array.isArray(from) ? pick(from) : pick([from])
|
|
111
|
+
const reducers = []
|
|
112
|
+
|
|
113
|
+
if (typeof using === 'string') reducers.push({ field: using, formula: identity })
|
|
114
|
+
if (typeof using === 'object')
|
|
115
|
+
Object.entries(using).forEach(([field, formula]) => reducers.push({ field, formula }))
|
|
116
|
+
|
|
117
|
+
config.summaries.push({ mapper, reducers })
|
|
118
|
+
|
|
119
|
+
return config
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Joins two datasets together based on a condition. Result includes all rows from the first
|
|
124
|
+
* dataset and matching rows from the second dataset. In case of multiple matches, all
|
|
125
|
+
* combinations are returned. When combining the rows, the columns from the first dataset take
|
|
126
|
+
* precedence.
|
|
127
|
+
*
|
|
128
|
+
* inner: only the rows that have a match in both datasets.
|
|
129
|
+
* outer: all rows from the first dataset and matching rows from the second dataset.
|
|
130
|
+
*
|
|
131
|
+
* @param {Array} first - The first dataset to join.
|
|
132
|
+
* @param {Array} second - The second dataset to join.
|
|
133
|
+
* @param {Function} condition - The condition to join the datasets on.
|
|
134
|
+
* @returns {Object} - An object with the inner and outer join result.
|
|
135
|
+
*/
|
|
136
|
+
function joinData(first, second, condition) {
|
|
137
|
+
let inner = []
|
|
138
|
+
const outer = []
|
|
139
|
+
|
|
140
|
+
first.forEach((f) => {
|
|
141
|
+
const matches = second.filter((s) => condition(f, s)).map((m) => ({ ...m, ...f }))
|
|
142
|
+
inner = inner.concat(matches)
|
|
143
|
+
if (matches.length === 0) outer.push(f)
|
|
144
|
+
})
|
|
145
|
+
return { inner, outer }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Filters the results of the first dataset based on the condition using the second dataset.
|
|
150
|
+
*
|
|
151
|
+
* @param {Array} first - The first dataset to filter.
|
|
152
|
+
* @param {Array} second - The second dataset to filter by.
|
|
153
|
+
* @param {Function} condition - The condition to filter the first dataset by.
|
|
154
|
+
* @returns {Array} - The filtered dataset.
|
|
155
|
+
*/
|
|
156
|
+
function antiJoin(first, second, condition) {
|
|
157
|
+
return first.filter((f) => !second.find((s) => condition(f, s)))
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Joins two datasets together based on a condition. Result includes all rows from the first
|
|
162
|
+
* dataset and matching rows from the second dataset. In case of multiple matches, all
|
|
163
|
+
* combinations are returned. When combining the rows, the columns from the first dataset take
|
|
164
|
+
* precedence.
|
|
165
|
+
*
|
|
166
|
+
* @param {Array} first - The first dataset to join.
|
|
167
|
+
* @param {Array} second - The second dataset to join.
|
|
168
|
+
* @param {Function} condition - The condition to join the datasets on.
|
|
169
|
+
* @returns {Array} - The joined dataset.
|
|
170
|
+
*/
|
|
171
|
+
function innerJoin(first, second, condition) {
|
|
172
|
+
const { inner } = joinData(first, second, condition)
|
|
173
|
+
return inner
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Performs a left join on two datasets based on a condition. Result includes all rows from the first
|
|
178
|
+
* dataset and matching rows from the second dataset. In case of multiple matches, all combinations
|
|
179
|
+
* are returned. When combining the rows, the columns from the first dataset take precedence.
|
|
180
|
+
* If there is no match in the second dataset, only the row from the first dataset is returned.
|
|
181
|
+
*
|
|
182
|
+
* @param {Array} first - The first dataset to join.
|
|
183
|
+
* @param {Array} second - The second dataset to join.
|
|
184
|
+
* @param {Function} condition - The condition to join the datasets on.
|
|
185
|
+
* @returns {Array} - The joined dataset.
|
|
186
|
+
*/
|
|
187
|
+
function leftJoin(first, second, condition) {
|
|
188
|
+
const { inner, outer } = joinData(first, second, condition)
|
|
189
|
+
return inner.concat(outer)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Performs a right join on two datasets based on a condition. Result includes all rows from the second
|
|
194
|
+
* dataset and matching rows from the first dataset. In case of multiple matches, all combinations
|
|
195
|
+
* are returned. When combining the rows, the columns from the second dataset take precedence.
|
|
196
|
+
* If there is no match in the first dataset, only the row from the second dataset is returned.
|
|
197
|
+
*
|
|
198
|
+
* @param {Array} first - The first dataset to join.
|
|
199
|
+
* @param {Array} second - The second dataset to join.
|
|
200
|
+
* @param {Function} condition - The condition to join the datasets on.
|
|
201
|
+
* @returns {Array} - The joined dataset.
|
|
202
|
+
*/
|
|
203
|
+
function rightJoin(first, second, condition) {
|
|
204
|
+
const { inner, outer } = joinData(second, first, condition)
|
|
205
|
+
return inner.concat(outer)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Performs a full join on two datasets based on a condition. Result includes all rows from both
|
|
210
|
+
* datasets. In case of multiple matches, all combinations are returned. When combining the rows,
|
|
211
|
+
* the columns from the first dataset take precedence. If there is no match in the second dataset,
|
|
212
|
+
* only the row from the first dataset is returned. If there is no match in the first dataset, only
|
|
213
|
+
* the row from the second dataset is returned.
|
|
214
|
+
*
|
|
215
|
+
* @param {Array} first - The first dataset to join.
|
|
216
|
+
* @param {Array} second - The second dataset to join.
|
|
217
|
+
* @param {Function} condition - The condition to join the datasets on.
|
|
218
|
+
* @returns {Array} - The joined dataset.
|
|
219
|
+
*/
|
|
220
|
+
function fullJoin(first, second, condition) {
|
|
221
|
+
const { inner, outer } = joinData(first, second, condition)
|
|
222
|
+
const rightOuter = antiJoin(second, first, condition)
|
|
223
|
+
return inner.concat(outer).concat(rightOuter)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Performs a cross join on two datasets. Result includes all combinations of rows from both datasets.
|
|
228
|
+
*
|
|
229
|
+
* @param {Array} first - The first dataset to join.
|
|
230
|
+
* @param {Array} second - The second dataset to join.
|
|
231
|
+
* @returns {Array} - The joined dataset.
|
|
232
|
+
*/
|
|
233
|
+
function crossJoin(first, second) {
|
|
234
|
+
return first.map((f) => second.map((s) => ({ ...f, ...s }))).flat()
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Performs a semi join on two datasets based on a condition. Result includes all rows from the first
|
|
239
|
+
* dataset that have a match in the second dataset.
|
|
240
|
+
*
|
|
241
|
+
* @param {Array} first - The first dataset to join.
|
|
242
|
+
* @param {Array} second - The second dataset to join.
|
|
243
|
+
* @param {Function} condition - The condition to join the datasets on.
|
|
244
|
+
* @returns {Array} - The joined dataset.
|
|
245
|
+
*/
|
|
246
|
+
function semiJoin(first, second, condition) {
|
|
247
|
+
return first.filter((f) => second.find((s) => condition(f, s)))
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Performs a nested join on two datasets based on a condition. Result includes all rows from the first
|
|
252
|
+
* dataset that have a match in the second dataset. The result is nested with the matching rows from the
|
|
253
|
+
* second dataset.
|
|
254
|
+
*
|
|
255
|
+
* @param {Array} first - The first dataset to join.
|
|
256
|
+
* @param {Array} second - The second dataset to join.
|
|
257
|
+
* @param {Function} condition - The condition to join the datasets on.
|
|
258
|
+
* @param {String} [key='children'] - The key to nest the matching rows under.
|
|
259
|
+
* @returns {Array} - The joined dataset.
|
|
260
|
+
*/
|
|
261
|
+
function nestedJoin(first, second, condition, key = 'children') {
|
|
262
|
+
const result = first.map((f) => ({
|
|
263
|
+
...f,
|
|
264
|
+
[key]: second.filter((s) => condition(f, s))
|
|
265
|
+
}))
|
|
266
|
+
return result
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Renames the keys of the data based on the provided mapping, or function
|
|
271
|
+
* that returns the new key. If the new key already exists, the value is overwritten.
|
|
272
|
+
*
|
|
273
|
+
* @param {Array<Object>} data - The data to rename the keys of.
|
|
274
|
+
* @param {Object|Function} how - The mapping of old keys to new keys, or a function that performs renaming.
|
|
275
|
+
* returns Array<Object> - The updated data with renamed keys.
|
|
276
|
+
*/
|
|
277
|
+
function renameKeys(data, how) {
|
|
278
|
+
const rename = typeof how === 'function' ? how : renameKeysUsingMap(how)
|
|
279
|
+
return data.map(rename)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Drops the specified keys from the data.
|
|
284
|
+
*
|
|
285
|
+
* @param {Array<Object>} data - The data to drop keys from.
|
|
286
|
+
* @param {Array<String>} keys - The keys to drop.
|
|
287
|
+
* @returns {Array<Object>} - The updated data with the specified keys dropped.
|
|
288
|
+
*/
|
|
289
|
+
function dropKeys(data, ...keys) {
|
|
290
|
+
return data.map((row) => omit(keys, row))
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Deletes rows from the data based on the condition
|
|
295
|
+
*
|
|
296
|
+
* @param {Array<Object>} data - The data to delete rows from.
|
|
297
|
+
* @param {Function} condition - The condition to delete rows on.
|
|
298
|
+
* @returns {Array<Object>} - The updated data with rows deleted based on the condition.
|
|
299
|
+
*/
|
|
300
|
+
function deleteRows(data, condition) {
|
|
301
|
+
return data.filter((row) => !condition(row))
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Updates rows in the data based on the condition
|
|
306
|
+
*
|
|
307
|
+
* @param {Array<Object>} data - The data to update rows in.
|
|
308
|
+
* @param {Function} filter - The condition to update rows on.
|
|
309
|
+
* @param {Object|Function} value - The values to update the rows with.
|
|
310
|
+
* @returns {Array<Object>} - The updated data with rows updated based on the condition.
|
|
311
|
+
*/
|
|
312
|
+
function updateRows(data, filter, value) {
|
|
313
|
+
if (typeof value !== 'function' && typeof value !== 'object') {
|
|
314
|
+
throw new Error('Value must be an object or function')
|
|
315
|
+
}
|
|
316
|
+
return data.map((row) =>
|
|
317
|
+
filter(row) ? (typeof value === 'function' ? value(row) : { ...row, ...value }) : row
|
|
318
|
+
)
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Fills missing values in the data based on the provided value.
|
|
323
|
+
*
|
|
324
|
+
* @param {Array<Object>} data - The data to fill missing values in.
|
|
325
|
+
* @param {Object} value - The value to fill missing values with.
|
|
326
|
+
* @returns {Array<Object>} - The updated data with missing values filled.
|
|
327
|
+
*/
|
|
328
|
+
function fillNA(data, value) {
|
|
329
|
+
/**
|
|
330
|
+
* Fills missing values in a single row based on the provided value.
|
|
331
|
+
*
|
|
332
|
+
* @param {Object} row - The row to fill missing values in.
|
|
333
|
+
*/
|
|
334
|
+
const fill = (row) => {
|
|
335
|
+
const filled = {}
|
|
336
|
+
Object.entries(value).forEach(([k, v]) => {
|
|
337
|
+
if (row[k] === undefined || row[k] === null) {
|
|
338
|
+
filled[k] = v
|
|
339
|
+
}
|
|
340
|
+
})
|
|
341
|
+
return { ...row, ...filled }
|
|
342
|
+
}
|
|
343
|
+
return data.map(fill)
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Returns a function that renames the keys of a single row based on the provided mapping.
|
|
348
|
+
*
|
|
349
|
+
* @param {Object} lookup - The mapping of old keys to new keys.
|
|
350
|
+
* @returns {Function} - The function that renames the keys of a single row.
|
|
351
|
+
*/
|
|
352
|
+
function renameKeysUsingMap(lookup) {
|
|
353
|
+
return (row) =>
|
|
354
|
+
Object.entries(row)
|
|
355
|
+
.sort((a, b) => descending(a[0], b[0]))
|
|
356
|
+
.reduce((acc, [k, v]) => ({ ...acc, [lookup[k] || k]: v }), {})
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Sorts data by the specified columns.
|
|
361
|
+
*
|
|
362
|
+
* @param {Array<Object>} data - The data to sort.
|
|
363
|
+
* @param {import('./types').SortableColumn} fields - The columns to sort by.
|
|
364
|
+
* @returns {Array<Object>} The sorted data
|
|
365
|
+
*/
|
|
366
|
+
function sortDataBy(data, ...fields) {
|
|
367
|
+
const sorters = fields.map(deriveSortableColumn)
|
|
368
|
+
|
|
369
|
+
data.sort((a, b) => {
|
|
370
|
+
let result = 0
|
|
371
|
+
for (let i = 0; i < sorters.length && result === 0; i++) {
|
|
372
|
+
const { name, sorter } = sorters[i]
|
|
373
|
+
result = sorter(a[name], b[name])
|
|
374
|
+
}
|
|
375
|
+
return result
|
|
376
|
+
})
|
|
377
|
+
return data
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Selects the specified columns from the dataset.
|
|
382
|
+
*
|
|
383
|
+
* @param {Array<Object>} data - The data to select from.
|
|
384
|
+
* @param {Object} config - The configuration object.
|
|
385
|
+
* @param {Function} config.filter - The filter function to apply to the data.
|
|
386
|
+
* @param {Array<String>} cols - The columns to select.
|
|
387
|
+
* @returns {Array<Object>} The selected data.
|
|
388
|
+
*/
|
|
389
|
+
function selectKeys(data, config, ...cols) {
|
|
390
|
+
const result = config.filter ? data.filter(config.filter) : data
|
|
391
|
+
if (cols.length > 0) return result.map(pick(cols))
|
|
392
|
+
return result
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Summarizes the dataset by the specified columns.
|
|
397
|
+
* @param {import('./types').dataset} df - The dataset object to summarize.
|
|
398
|
+
* @param {Array} summaries - The columns to summarize.
|
|
399
|
+
*
|
|
400
|
+
* @returns {import('./types').dataset} The summarized dataset object.
|
|
401
|
+
*/
|
|
402
|
+
function rollup(data, config) {
|
|
403
|
+
if (config.group_by.length === 0 && config.summaries.length === 0) {
|
|
404
|
+
throw new Error(
|
|
405
|
+
'Use groupBy to specify the columns to group by or use summarize to add aggregators.'
|
|
406
|
+
)
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const summaries = clone(config.summaries)
|
|
410
|
+
const hasAlignBy = config.align_by.length > 0
|
|
411
|
+
if (summaries.length === 0) summaries.push(defaultAggregator(config))
|
|
412
|
+
|
|
413
|
+
let alignedData = groupDataByKeys(data, config.group_by, summaries)
|
|
414
|
+
|
|
415
|
+
if (hasAlignBy) {
|
|
416
|
+
const fillRows = getAlignGenerator(data, config)
|
|
417
|
+
alignedData = fillAlignedData(alignedData, config, fillRows)
|
|
418
|
+
alignedData.forEach((group) => {
|
|
419
|
+
group[config.children] = sortDataBy(group[config.children], ...config.align_by)
|
|
420
|
+
})
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const aggregatedData = aggregateData(alignedData, summaries)
|
|
424
|
+
// const newMetadata = buildMetadata(aggregatedData, df.metadata, df.config.group_by, summaries)
|
|
425
|
+
|
|
426
|
+
return aggregatedData
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Returns the default aggregator which rolls up the columns other than the group_by columns into a single object.
|
|
431
|
+
*
|
|
432
|
+
* @param {import('./types').Metadata} metadata - The metadata for the columns to be aggregated.
|
|
433
|
+
* @param {Object} config - The configuration used to build the aggregator.
|
|
434
|
+
*
|
|
435
|
+
* @returns {Object} An object containing the default aggregator for the specified metadata and configuration.
|
|
436
|
+
*/
|
|
437
|
+
export function defaultAggregator(config) {
|
|
438
|
+
return {
|
|
439
|
+
mapper: omit(config.group_by),
|
|
440
|
+
reducers: [{ field: config.children, formula: identity }]
|
|
441
|
+
}
|
|
442
|
+
}
|
package/src/filter.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { filterOperations } from './constants'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Filters an array of objects based on a specified condition defined in the options.
|
|
5
|
+
* If a column is provided, it filters based on the value of that column. Otherwise, it filters
|
|
6
|
+
* based on a value present in any of the object's properties.
|
|
7
|
+
*
|
|
8
|
+
* @param {Object[]} data - The array of objects to filter.
|
|
9
|
+
* @param {Object} options - The filtering options that define the condition.
|
|
10
|
+
* @param {string} [options.column] - The property name in the objects to filter by. If omitted, all properties are considered.
|
|
11
|
+
* @param {string|number} [options.value] - The value to compare against when filtering.
|
|
12
|
+
* @param {string} [options.operator] - The operator to use for comparison, should match a key in the filterOperations object.
|
|
13
|
+
* @returns {Object[]} - The filtered array of objects.
|
|
14
|
+
*/
|
|
15
|
+
export function filterObjectArray(data, options) {
|
|
16
|
+
const { column, value, operator } = options
|
|
17
|
+
// If no operator or value is given, return the data unchanged.
|
|
18
|
+
if (!operator || !value) return data
|
|
19
|
+
// If a column is specified, perform the filtering on that specific column.
|
|
20
|
+
if (column) return data.filter((row) => filterOperations[operator](row[column], value))
|
|
21
|
+
|
|
22
|
+
return data.filter((row) => findMatchingRow(row, value, operator))
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Checks if a row matches the given value based on the specified operator.
|
|
27
|
+
* Supports both standard and negated operators by checking the operator string
|
|
28
|
+
* for a leading '!' character, which indicates negation.
|
|
29
|
+
*
|
|
30
|
+
* @param {Object} row - The row object in which to search for the value match.
|
|
31
|
+
* @param {any} value - The value to match against the row's properties.
|
|
32
|
+
* @param {string} operator - The operator used to compare the row's properties to the value.
|
|
33
|
+
* When operator starts with '!', it negates the result of the comparison.
|
|
34
|
+
* @returns {boolean} - Returns true if the row matches the condition; otherwise, returns false.
|
|
35
|
+
*/
|
|
36
|
+
function findMatchingRow(row, value, operator) {
|
|
37
|
+
const op = operator.startsWith('!') ? operator.slice(1) : operator
|
|
38
|
+
const matched = Object.keys(row).some((key) => filterOperations[op](row[key], value))
|
|
39
|
+
|
|
40
|
+
return operator.startsWith('!') ? !matched : matched
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Applies multiple filtering conditions to an array of objects. This function iterates through each filter option,
|
|
45
|
+
* successively narrowing down the data.
|
|
46
|
+
*
|
|
47
|
+
* @param {Object[]} data - The array of objects to filter.
|
|
48
|
+
* @param {Object[]} filters - An array of filtering options to apply to the data.
|
|
49
|
+
* @returns {Object[]} - The filtered array of objects that meet all conditions specified by the filters.
|
|
50
|
+
*/
|
|
51
|
+
export function filterData(data, filters) {
|
|
52
|
+
let filteredData = data
|
|
53
|
+
filters.forEach((filter) => {
|
|
54
|
+
filteredData = filterObjectArray(filteredData, filter)
|
|
55
|
+
})
|
|
56
|
+
return filteredData
|
|
57
|
+
}
|
package/src/formatter.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { identity } from 'ramda'
|
|
2
|
+
|
|
3
|
+
const formatterMap = {
|
|
4
|
+
currency: getCurrencyFormatter,
|
|
5
|
+
integer: (language) => (val) => val.toLocaleString(language, { maximumFractionDigits: 0 }),
|
|
6
|
+
number: (language, decimalPlaces) => (val) =>
|
|
7
|
+
val.toLocaleString(language, {
|
|
8
|
+
minimumFractionDigits: decimalPlaces,
|
|
9
|
+
maximumFractionDigits: decimalPlaces
|
|
10
|
+
}),
|
|
11
|
+
date: (language) => (val) => val.toLocaleDateString(language),
|
|
12
|
+
time: (language) => (val) => val.toLocaleTimeString(language),
|
|
13
|
+
object: () => JSON.stringify,
|
|
14
|
+
array: () => JSON.stringify, // Combined formatter for 'object' and 'array'
|
|
15
|
+
ellipsis: () => () => '...' // Static formatter, no need for parameters
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Returns a currency formatter function that formats a value as a currency string.
|
|
20
|
+
* @param {string} language - The IETF language tag used for locale-specific formatting.
|
|
21
|
+
* @param {number} decimalPlaces - The number of decimal places to show with currency formatting.
|
|
22
|
+
* @returns {function} - A currency formatter function that takes a value and returns a formatted string.
|
|
23
|
+
*/
|
|
24
|
+
function getCurrencyFormatter(language, decimalPlaces) {
|
|
25
|
+
/**
|
|
26
|
+
* Formats a value as a currency string with the desired decimal places.
|
|
27
|
+
*
|
|
28
|
+
* @param {number} val - The value to format as a currency string.
|
|
29
|
+
* @param {string} [currency='USD'] - The currency code to use for formatting.
|
|
30
|
+
* @returns {string} - The formatted currency string.
|
|
31
|
+
*/
|
|
32
|
+
const formatter = (val, currency = 'USD') =>
|
|
33
|
+
val.toLocaleString(language, {
|
|
34
|
+
style: 'currency',
|
|
35
|
+
currency,
|
|
36
|
+
minimumFractionDigits: decimalPlaces,
|
|
37
|
+
maximumFractionDigits: decimalPlaces
|
|
38
|
+
})
|
|
39
|
+
return formatter
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Creates a formatter function that formats a value according to the specified type and optional locale settings.
|
|
44
|
+
* Supported types include 'default' (no formatting), 'integer', 'number', 'date', 'time',, 'object', 'array', 'currency' & 'ellipsis''
|
|
45
|
+
*
|
|
46
|
+
* @param {string} type - The type of the formatter to use (e.g., 'integer', 'number', 'date', 'time', 'currency').
|
|
47
|
+
* @param {string} [language='en-US'] - Optional IETF language tag used for locale-specific formatting.
|
|
48
|
+
* @param {number} [decimalPlaces=2] - Optional number of decimal places to show with number and currency formatting.
|
|
49
|
+
* @returns {*} - A format function that takes a value and returns a formatted string.
|
|
50
|
+
*/
|
|
51
|
+
export function createFormatter(type, language = 'en-US', decimalPlaces = 2) {
|
|
52
|
+
const formatter = formatterMap[type]
|
|
53
|
+
if (formatter) return formatter(language, decimalPlaces)
|
|
54
|
+
|
|
55
|
+
return identity
|
|
56
|
+
}
|
package/src/hierarchy.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { defaultViewOptions } from './constants'
|
|
2
|
+
/**
|
|
3
|
+
* Removes objects from an array where the parent attribute is not null.
|
|
4
|
+
* @param {Array<Object>} arr - The array of objects.
|
|
5
|
+
*/
|
|
6
|
+
export function removeChildren(arr) {
|
|
7
|
+
for (let i = arr.length - 1; i >= 0; i--) {
|
|
8
|
+
if (arr[i].parent) arr.splice(i, 1)
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Flatten nested children arrays within an array of objects.
|
|
13
|
+
* @param {Array<Object>} arr - The array of objects.
|
|
14
|
+
*/
|
|
15
|
+
export function flattenNestedChildren(arr) {
|
|
16
|
+
let index = 0
|
|
17
|
+
while (index < arr.length) {
|
|
18
|
+
if (Array.isArray(arr[index].children) && arr[index].children.length > 0) {
|
|
19
|
+
const children = arr[index].children
|
|
20
|
+
arr.splice(index + 1, 0, ...children)
|
|
21
|
+
}
|
|
22
|
+
index++
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Recursively updates parent items' flags based on the child's flags.
|
|
28
|
+
* @param {Object} item - The current object.
|
|
29
|
+
*/
|
|
30
|
+
function updateParentFlags(item) {
|
|
31
|
+
if (!item.parent) return
|
|
32
|
+
if (item.parent.excluded) {
|
|
33
|
+
item.parent.excluded = false
|
|
34
|
+
item.parent.retainedByChild = true
|
|
35
|
+
|
|
36
|
+
updateParentFlags(item.parent)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Applies a hierarchical filter to an array of objects.
|
|
42
|
+
* @param {Array<Object>} arr - The array of objects.
|
|
43
|
+
* @param {Function} filterFn - The filter function to apply on each object.
|
|
44
|
+
*/
|
|
45
|
+
export function hierarchicalFilter(arr, filterFn) {
|
|
46
|
+
arr.forEach((item) => {
|
|
47
|
+
item.excluded = !filterFn(item)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
arr.forEach((item) => {
|
|
51
|
+
if (!item.excluded) updateParentFlags(item)
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Derives the hierarchy from the data.
|
|
57
|
+
*
|
|
58
|
+
* @param {Array} data - The data to derive the hierarchy from.
|
|
59
|
+
* @param {string} path - The column name to be used as hierarchical path.
|
|
60
|
+
* @param {string} separator - The separator to be used in the path.
|
|
61
|
+
* @returns {Array<import('./types').Hierarchy>} - The derived hierarchy.
|
|
62
|
+
*/
|
|
63
|
+
export function deriveHierarchy(data, options) {
|
|
64
|
+
const { expanded, path, separator } = { ...defaultViewOptions, ...options }
|
|
65
|
+
if (!path) return data.map((row) => ({ depth: 0, row }))
|
|
66
|
+
|
|
67
|
+
const hierarchy = data.map((row) => {
|
|
68
|
+
const parts = row[path].split(separator).filter((part) => part.length > 0)
|
|
69
|
+
const depth = parts.length
|
|
70
|
+
const value = depth > 0 ? parts[depth - 1] : ''
|
|
71
|
+
return { depth, value, path: row[path], row }
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
hierarchy.forEach((row) => {
|
|
75
|
+
row.children = hierarchy.filter(
|
|
76
|
+
(child) => child.path.startsWith(row.path) && row.depth === child.depth - 1
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
row.isParent = row.children.length > 0
|
|
80
|
+
if (row.isParent) {
|
|
81
|
+
row.isExpanded = expanded
|
|
82
|
+
row.isHidden = !row.parent ? false : row.parent.isHidden || !row.parent.isExpanded
|
|
83
|
+
}
|
|
84
|
+
row.children.forEach((child) => {
|
|
85
|
+
child.parent = row
|
|
86
|
+
child.isHidden = row.isHidden || !row.isExpanded
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
return hierarchy
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Recursively sets the visibility of child nodes based on the hidden and expanded state
|
|
94
|
+
* of their parent node.
|
|
95
|
+
*
|
|
96
|
+
* @param {Object} node - The parent node whose children's visibility will be changed.
|
|
97
|
+
* The node is expected to have a 'children' property which is an
|
|
98
|
+
* array of child nodes, a 'isHidden' property indicating if the
|
|
99
|
+
* node is hidden, and a 'isExpanded' property indicating if the
|
|
100
|
+
* node is expanded.
|
|
101
|
+
*/
|
|
102
|
+
function changeVisibilityForChildren(node) {
|
|
103
|
+
if (node.children) {
|
|
104
|
+
node.children.forEach((child) => {
|
|
105
|
+
child.isHidden = node.isHidden || !node.isExpanded
|
|
106
|
+
changeVisibilityForChildren(child)
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Toggles the expansion state of a node and updates the visibility of its children accordingly.
|
|
113
|
+
* If the node is a parent (has child nodes), it will invert its 'isExpanded' property and call
|
|
114
|
+
* 'changeVisibilityForChildren' to reflect changes to its child nodes.
|
|
115
|
+
*
|
|
116
|
+
* @param {Object} node - The node to toggle expansion for. The node is expected to have an
|
|
117
|
+
* 'isParent' property indicating if the node has children, an
|
|
118
|
+
* 'isExpanded' property indicating if the node is expanded, and an array
|
|
119
|
+
* of child nodes under the 'children' property if it is a parent.
|
|
120
|
+
*/
|
|
121
|
+
export function toggleExpansion(node) {
|
|
122
|
+
if (node.isParent) {
|
|
123
|
+
node.isExpanded = !node.isExpanded
|
|
124
|
+
changeVisibilityForChildren(node)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// function hierarchy(data, options){
|
|
129
|
+
// const nodes = deriveHierarchy(data, options)
|
|
130
|
+
|
|
131
|
+
// return {
|
|
132
|
+
// nodes,
|
|
133
|
+
// filter: (fn) => hierarchicalFilter(nodes, fn),
|
|
134
|
+
// selection:
|
|
135
|
+
// }
|
|
136
|
+
// }
|