@rokkit/data 1.0.0-next.148 → 1.0.0-next.150
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/dist/benchmark/fixtures.d.ts +33 -0
- package/dist/benchmark/index.d.ts +2 -0
- package/dist/benchmark/runner.d.ts +10 -0
- package/dist/{constants.d.ts → src/constants.d.ts} +6 -7
- package/dist/{dataset.d.ts → src/dataset.d.ts} +33 -15
- package/dist/src/dataset.svelte.d.ts +79 -0
- package/dist/{index.d.ts → src/index.d.ts} +1 -1
- package/dist/{infer.d.ts → src/infer.d.ts} +39 -0
- package/dist/{model.d.ts → src/model.d.ts} +1 -1
- package/dist/src/pipeline.d.ts +8 -0
- package/dist/{rollup.d.ts → src/rollup.d.ts} +2 -12
- package/package.json +1 -1
- package/src/constants.js +9 -4
- package/src/dataset.js +148 -119
- package/src/dataset.svelte.js +181 -0
- package/src/formatter.js +1 -1
- package/src/index.js +9 -1
- package/src/infer.js +103 -2
- package/src/join.js +11 -6
- package/src/model.js +3 -3
- package/src/pipeline.js +97 -0
- package/src/renamer.js +8 -1
- package/src/rollup.js +59 -62
- package/src/view.js +1 -2
- /package/dist/{aggregators.d.ts → src/aggregators.d.ts} +0 -0
- /package/dist/{convert.d.ts → src/convert.d.ts} +0 -0
- /package/dist/{filter.d.ts → src/filter.d.ts} +0 -0
- /package/dist/{formatter.d.ts → src/formatter.d.ts} +0 -0
- /package/dist/{hierarchy.d.ts → src/hierarchy.d.ts} +0 -0
- /package/dist/{join.d.ts → src/join.d.ts} +0 -0
- /package/dist/{list.d.ts → src/list.d.ts} +0 -0
- /package/dist/{metadata.d.ts → src/metadata.d.ts} +0 -0
- /package/dist/{parser.d.ts → src/parser.d.ts} +0 -0
- /package/dist/{renamer.d.ts → src/renamer.d.ts} +0 -0
- /package/dist/{types.d.ts → src/types.d.ts} +0 -0
- /package/dist/{utils.d.ts → src/utils.d.ts} +0 -0
- /package/dist/{view.d.ts → src/view.d.ts} +0 -0
package/src/dataset.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { equals, identity, pick, omit, clone } from 'ramda'
|
|
2
1
|
import { descending } from 'd3-array'
|
|
3
2
|
import { pickAllowedConfig, defaultConfig } from './constants'
|
|
4
|
-
import {
|
|
3
|
+
import { deriveColumnDefs } from './infer'
|
|
4
|
+
import { executeFused, sortDataByFields } from './pipeline.js'
|
|
5
5
|
import { groupDataByKeys, fillAlignedData, getAlignGenerator, aggregateData } from './rollup'
|
|
6
6
|
import {
|
|
7
7
|
leftJoin,
|
|
@@ -14,19 +14,54 @@ import {
|
|
|
14
14
|
nestedJoin
|
|
15
15
|
} from './join'
|
|
16
16
|
|
|
17
|
+
// ── Native helpers (replace Ramda) ─────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
function pickKeys(keys) {
|
|
20
|
+
return (row) => {
|
|
21
|
+
const out = {}
|
|
22
|
+
for (const k of keys) if (k in row) out[k] = row[k]
|
|
23
|
+
return out
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function omitKeys(keys) {
|
|
28
|
+
const skip = new Set(keys)
|
|
29
|
+
return (row) => {
|
|
30
|
+
const out = {}
|
|
31
|
+
for (const k of Object.keys(row)) if (!skip.has(k)) out[k] = row[k]
|
|
32
|
+
return out
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function deepEqual(a, b) {
|
|
37
|
+
return JSON.stringify(a) === JSON.stringify(b)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function cloneConfig(config) {
|
|
41
|
+
return {
|
|
42
|
+
...config,
|
|
43
|
+
group_by: [...config.group_by],
|
|
44
|
+
align_by: [...config.align_by],
|
|
45
|
+
summaries: [...config.summaries],
|
|
46
|
+
template: { ...config.template }
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
51
|
+
|
|
17
52
|
/**
|
|
18
53
|
* Adds a summary field to the dataset.
|
|
19
|
-
*
|
|
20
|
-
* @param {Object} config - The configuration object to be updated.
|
|
21
|
-
* @param {string|Array<string>|Function} from - The field or function to fetch data for summary
|
|
22
|
-
* @param {string|Object<string:Function>} using - The target field & formula to use for summarizing.
|
|
23
|
-
* @returns {Object} - The updated configuration.
|
|
24
54
|
*/
|
|
25
55
|
function summarize(config, from, using) {
|
|
26
|
-
const mapper =
|
|
56
|
+
const mapper =
|
|
57
|
+
typeof from === 'function'
|
|
58
|
+
? from
|
|
59
|
+
: Array.isArray(from)
|
|
60
|
+
? pickKeys(from)
|
|
61
|
+
: pickKeys([from])
|
|
27
62
|
const reducers = []
|
|
28
63
|
|
|
29
|
-
if (typeof using === 'string') reducers.push({ field: using, formula:
|
|
64
|
+
if (typeof using === 'string') reducers.push({ field: using, formula: (x) => x })
|
|
30
65
|
if (typeof using === 'object')
|
|
31
66
|
Object.entries(using).forEach(([field, formula]) => reducers.push({ field, formula }))
|
|
32
67
|
|
|
@@ -37,11 +72,6 @@ function summarize(config, from, using) {
|
|
|
37
72
|
|
|
38
73
|
/**
|
|
39
74
|
* Updates rows in the data based on the condition
|
|
40
|
-
*
|
|
41
|
-
* @param {Array<Object>} data - The data to update rows in.
|
|
42
|
-
* @param {Function} filter - The condition to update rows on.
|
|
43
|
-
* @param {Object|Function} value - The values to update the rows with.
|
|
44
|
-
* @returns {Array<Object>} - The updated data with rows updated based on the condition.
|
|
45
75
|
*/
|
|
46
76
|
function updateRows(data, filter, value) {
|
|
47
77
|
if (typeof value !== 'function' && typeof value !== 'object') {
|
|
@@ -54,17 +84,8 @@ function updateRows(data, filter, value) {
|
|
|
54
84
|
|
|
55
85
|
/**
|
|
56
86
|
* Fills missing values in the data based on the provided value.
|
|
57
|
-
*
|
|
58
|
-
* @param {Array<Object>} data - The data to fill missing values in.
|
|
59
|
-
* @param {Object} value - The value to fill missing values with.
|
|
60
|
-
* @returns {Array<Object>} - The updated data with missing values filled.
|
|
61
87
|
*/
|
|
62
88
|
function fillNA(data, value) {
|
|
63
|
-
/**
|
|
64
|
-
* Fills missing values in a single row based on the provided value.
|
|
65
|
-
*
|
|
66
|
-
* @param {Object} row - The row to fill missing values in.
|
|
67
|
-
*/
|
|
68
89
|
const fill = (row) => {
|
|
69
90
|
const filled = {}
|
|
70
91
|
Object.entries(value).forEach(([k, v]) => {
|
|
@@ -79,9 +100,6 @@ function fillNA(data, value) {
|
|
|
79
100
|
|
|
80
101
|
/**
|
|
81
102
|
* Returns a function that renames the keys of a single row based on the provided mapping.
|
|
82
|
-
*
|
|
83
|
-
* @param {Object} lookup - The mapping of old keys to new keys.
|
|
84
|
-
* @returns {Function} - The function that renames the keys of a single row.
|
|
85
103
|
*/
|
|
86
104
|
function renameKeysUsingMap(lookup) {
|
|
87
105
|
return (row) =>
|
|
@@ -91,12 +109,7 @@ function renameKeysUsingMap(lookup) {
|
|
|
91
109
|
}
|
|
92
110
|
|
|
93
111
|
/**
|
|
94
|
-
* Renames the keys of the data based on the provided mapping, or function
|
|
95
|
-
* that returns the new key. If the new key already exists, the value is overwritten.
|
|
96
|
-
*
|
|
97
|
-
* @param {Array<Object>} data - The data to rename the keys of.
|
|
98
|
-
* @param {Object|Function} how - The mapping of old keys to new keys, or a function that performs renaming.
|
|
99
|
-
* returns Array<Object> - The updated data with renamed keys.
|
|
112
|
+
* Renames the keys of the data based on the provided mapping, or function.
|
|
100
113
|
*/
|
|
101
114
|
function renameKeys(data, how) {
|
|
102
115
|
const rename = typeof how === 'function' ? how : renameKeysUsingMap(how)
|
|
@@ -104,47 +117,17 @@ function renameKeys(data, how) {
|
|
|
104
117
|
}
|
|
105
118
|
|
|
106
119
|
/**
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
* @param {Array<Object>} data - The data to sort.
|
|
110
|
-
* @param {import('./types').SortableColumn} fields - The columns to sort by.
|
|
111
|
-
* @returns {Array<Object>} The sorted data
|
|
112
|
-
*/
|
|
113
|
-
function sortDataBy(data, ...fields) {
|
|
114
|
-
const sorters = fields.map(deriveSortableColumn)
|
|
115
|
-
|
|
116
|
-
data.sort((a, b) => {
|
|
117
|
-
let result = 0
|
|
118
|
-
for (let i = 0; i < sorters.length && result === 0; i++) {
|
|
119
|
-
const { name, sorter } = sorters[i]
|
|
120
|
-
result = sorter(a[name], b[name])
|
|
121
|
-
}
|
|
122
|
-
return result
|
|
123
|
-
})
|
|
124
|
-
return data
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Returns the default aggregator which rolls up the columns other than the group_by columns into a single object.
|
|
129
|
-
*
|
|
130
|
-
* @param {import('./types').Metadata} metadata - The metadata for the columns to be aggregated.
|
|
131
|
-
* @param {Object} config - The configuration used to build the aggregator.
|
|
132
|
-
*
|
|
133
|
-
* @returns {Object} An object containing the default aggregator for the specified metadata and configuration.
|
|
120
|
+
* Returns the default aggregator which rolls up non-group_by columns into a children array.
|
|
134
121
|
*/
|
|
135
122
|
export function defaultAggregator(config) {
|
|
136
123
|
return {
|
|
137
|
-
mapper:
|
|
138
|
-
reducers: [{ field: config.children, formula:
|
|
124
|
+
mapper: omitKeys(config.group_by),
|
|
125
|
+
reducers: [{ field: config.children, formula: (x) => x }]
|
|
139
126
|
}
|
|
140
127
|
}
|
|
141
128
|
|
|
142
129
|
/**
|
|
143
130
|
* Summarizes the dataset by the specified columns.
|
|
144
|
-
* @param {import('./types').dataset} df - The dataset object to summarize.
|
|
145
|
-
* @param {Array} summaries - The columns to summarize.
|
|
146
|
-
*
|
|
147
|
-
* @returns {import('./types').dataset} The summarized dataset object.
|
|
148
131
|
*/
|
|
149
132
|
function rollup(data, config) {
|
|
150
133
|
if (config.group_by.length === 0 && config.summaries.length === 0) {
|
|
@@ -153,7 +136,7 @@ function rollup(data, config) {
|
|
|
153
136
|
)
|
|
154
137
|
}
|
|
155
138
|
|
|
156
|
-
const summaries =
|
|
139
|
+
const summaries = [...config.summaries]
|
|
157
140
|
const hasAlignBy = config.align_by.length > 0
|
|
158
141
|
if (summaries.length === 0) summaries.push(defaultAggregator(config))
|
|
159
142
|
|
|
@@ -163,19 +146,18 @@ function rollup(data, config) {
|
|
|
163
146
|
const fillRows = getAlignGenerator(data, config)
|
|
164
147
|
alignedData = fillAlignedData(alignedData, config, fillRows)
|
|
165
148
|
alignedData.forEach((group) => {
|
|
166
|
-
group[config.children] =
|
|
149
|
+
group[config.children] = sortDataByFields(group[config.children], ...config.align_by)
|
|
167
150
|
})
|
|
168
151
|
}
|
|
169
152
|
|
|
170
|
-
|
|
171
|
-
// const newMetadata = buildMetadata(aggregatedData, df.metadata, df.config.group_by, summaries)
|
|
172
|
-
|
|
173
|
-
return aggregatedData
|
|
153
|
+
return aggregateData(alignedData, summaries)
|
|
174
154
|
}
|
|
175
155
|
|
|
176
156
|
export class DataSet {
|
|
177
|
-
#config =
|
|
157
|
+
#config = cloneConfig(defaultConfig)
|
|
178
158
|
#data = []
|
|
159
|
+
/** @type {Array<{type:string, fn?:Function, fields?:string[]}>} */
|
|
160
|
+
#ops = []
|
|
179
161
|
|
|
180
162
|
constructor(data) {
|
|
181
163
|
this.#data = data
|
|
@@ -198,110 +180,157 @@ export class DataSet {
|
|
|
198
180
|
return this
|
|
199
181
|
}
|
|
200
182
|
|
|
183
|
+
// ── Lazy linear ops (fused in select) ──────────────────────────────────
|
|
184
|
+
|
|
201
185
|
where(condition) {
|
|
202
|
-
this.#
|
|
203
|
-
return this
|
|
204
|
-
}
|
|
205
|
-
groupBy(...fields) {
|
|
206
|
-
this.#config.group_by = fields
|
|
207
|
-
return this
|
|
208
|
-
}
|
|
209
|
-
alignBy(...fields) {
|
|
210
|
-
this.#config.align_by = fields
|
|
186
|
+
this.#ops.push({ type: 'filter', fn: condition })
|
|
211
187
|
return this
|
|
212
188
|
}
|
|
213
|
-
|
|
214
|
-
this.#
|
|
189
|
+
apply(callback) {
|
|
190
|
+
this.#ops.push({ type: 'map', fn: callback })
|
|
215
191
|
return this
|
|
216
192
|
}
|
|
217
|
-
|
|
218
|
-
this.
|
|
193
|
+
sortBy(...fields) {
|
|
194
|
+
this.#ops.push({ type: 'sort', fields })
|
|
219
195
|
return this
|
|
220
196
|
}
|
|
221
197
|
|
|
198
|
+
// ── Eager ops (commit to this.#data immediately) ────────────────────────
|
|
199
|
+
|
|
222
200
|
rename(how) {
|
|
223
|
-
this
|
|
201
|
+
this.#data = renameKeys(this.#data, how)
|
|
224
202
|
return this
|
|
225
203
|
}
|
|
226
204
|
drop(...fields) {
|
|
227
|
-
const
|
|
228
|
-
newDataSet
|
|
205
|
+
const omit = omitKeys(fields)
|
|
206
|
+
const newDataSet = new DataSet(this.#data.map(omit))
|
|
207
|
+
newDataSet.config = this.#config
|
|
229
208
|
return newDataSet
|
|
230
209
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
this.data = sortDataBy(this.data, ...fields)
|
|
210
|
+
fillNA(value) {
|
|
211
|
+
this.#data = fillNA(this.#data, value)
|
|
234
212
|
return this
|
|
235
213
|
}
|
|
214
|
+
|
|
215
|
+
// ── Mutation ops (flush lazy ops first, then mutate) ───────────────────
|
|
216
|
+
|
|
236
217
|
remove() {
|
|
237
|
-
|
|
238
|
-
|
|
218
|
+
// Flush pending ops so the filter context is respected
|
|
219
|
+
const pending = this.#ops.splice(0)
|
|
220
|
+
if (pending.length > 0) {
|
|
221
|
+
// The last filter op is the one to use for removal
|
|
222
|
+
const filterOp = [...pending].reverse().find((o) => o.type === 'filter')
|
|
223
|
+
if (filterOp) {
|
|
224
|
+
this.#data = this.#data.filter((row) => !filterOp.fn(row))
|
|
225
|
+
} else {
|
|
226
|
+
this.#data = []
|
|
227
|
+
}
|
|
239
228
|
} else {
|
|
240
229
|
this.#data = []
|
|
241
230
|
}
|
|
242
|
-
this.#config.filter = undefined
|
|
243
|
-
// this.data = deleteRows(this.data, this.#config.filter || includeAll)
|
|
244
231
|
return this
|
|
245
232
|
}
|
|
246
233
|
update(value) {
|
|
247
|
-
|
|
248
|
-
this.#
|
|
234
|
+
// Flush pending ops to get the filter
|
|
235
|
+
const pending = this.#ops.splice(0)
|
|
236
|
+
const filterOp = [...pending].reverse().find((o) => o.type === 'filter')
|
|
237
|
+
const filter = filterOp ? filterOp.fn : () => true
|
|
238
|
+
this.#data = updateRows(this.#data, filter, value)
|
|
249
239
|
return this
|
|
250
240
|
}
|
|
251
|
-
|
|
252
|
-
|
|
241
|
+
|
|
242
|
+
// ── Aggregation ─────────────────────────────────────────────────────────
|
|
243
|
+
|
|
244
|
+
groupBy(...fields) {
|
|
245
|
+
this.#config.group_by = fields
|
|
253
246
|
return this
|
|
254
247
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
248
|
+
alignBy(...fields) {
|
|
249
|
+
this.#config.align_by = fields
|
|
250
|
+
return this
|
|
251
|
+
}
|
|
252
|
+
usingTemplate(template) {
|
|
253
|
+
this.#config.template = template
|
|
254
|
+
return this
|
|
255
|
+
}
|
|
256
|
+
summarize(from, fields) {
|
|
257
|
+
this.config = summarize(this.#config, from, fields)
|
|
258
258
|
return this
|
|
259
259
|
}
|
|
260
260
|
rollup() {
|
|
261
|
-
|
|
261
|
+
const data = executeFused(this.#data, this.#ops)
|
|
262
|
+
this.#ops = []
|
|
263
|
+
return new DataSet(rollup(data, this.#config))
|
|
262
264
|
}
|
|
265
|
+
|
|
266
|
+
// ── Terminal ────────────────────────────────────────────────────────────
|
|
267
|
+
|
|
263
268
|
select(...cols) {
|
|
264
|
-
const result = this.#
|
|
265
|
-
|
|
269
|
+
const result = executeFused(this.#data, this.#ops)
|
|
270
|
+
this.#ops = []
|
|
271
|
+
if (cols.length > 0) {
|
|
272
|
+
const pick = pickKeys(cols)
|
|
273
|
+
return result.map(pick)
|
|
274
|
+
}
|
|
266
275
|
return result
|
|
267
276
|
}
|
|
268
|
-
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Derive column definitions from the current data.
|
|
280
|
+
*
|
|
281
|
+
* Returns `{ name, type, scale, sortable, filterable, fields, formatter }` for each
|
|
282
|
+
* column, plus any user-supplied properties from `options.enhancements`.
|
|
283
|
+
*
|
|
284
|
+
* @param {Object} [options]
|
|
285
|
+
* @param {Array<Object>} [options.enhancements=[]] - Per-column overrides keyed by name.
|
|
286
|
+
* @param {string} [options.language='en-US'] - Locale for formatters.
|
|
287
|
+
* @param {string} [options.scanMode='fast'] - 'fast' or 'deep'.
|
|
288
|
+
* @returns {Array<Object>}
|
|
289
|
+
*/
|
|
290
|
+
columnDefs(options = {}) {
|
|
291
|
+
return deriveColumnDefs(this.#data, options)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ── Set operations ──────────────────────────────────────────────────────
|
|
295
|
+
|
|
269
296
|
union(other) {
|
|
270
297
|
if (Array.isArray(other)) {
|
|
271
|
-
return new DataSet(this
|
|
298
|
+
return new DataSet(this.#data.concat(other))
|
|
272
299
|
}
|
|
273
|
-
return new DataSet(this
|
|
300
|
+
return new DataSet(this.#data.concat(other.data))
|
|
274
301
|
}
|
|
275
302
|
minus(other) {
|
|
276
|
-
return new DataSet(this
|
|
303
|
+
return new DataSet(this.#data.filter((d) => !other.find((x) => deepEqual(x, d))))
|
|
277
304
|
}
|
|
278
305
|
intersect(other) {
|
|
279
|
-
return new DataSet(this
|
|
306
|
+
return new DataSet(this.#data.filter((d) => other.find((x) => deepEqual(x, d))))
|
|
280
307
|
}
|
|
281
|
-
|
|
308
|
+
|
|
309
|
+
// ── Joins ───────────────────────────────────────────────────────────────
|
|
310
|
+
|
|
282
311
|
innerJoin(other, condition) {
|
|
283
|
-
return new DataSet(innerJoin(this
|
|
312
|
+
return new DataSet(innerJoin(this.#data, other.data, condition))
|
|
284
313
|
}
|
|
285
314
|
leftJoin(other, condition) {
|
|
286
|
-
return new DataSet(leftJoin(this
|
|
315
|
+
return new DataSet(leftJoin(this.#data, other.data, condition))
|
|
287
316
|
}
|
|
288
317
|
rightJoin(other, condition) {
|
|
289
|
-
return new DataSet(rightJoin(this
|
|
318
|
+
return new DataSet(rightJoin(this.#data, other.data, condition))
|
|
290
319
|
}
|
|
291
320
|
fullJoin(other, condition) {
|
|
292
|
-
return new DataSet(fullJoin(this
|
|
321
|
+
return new DataSet(fullJoin(this.#data, other.data, condition))
|
|
293
322
|
}
|
|
294
323
|
crossJoin(other) {
|
|
295
|
-
return new DataSet(crossJoin(this
|
|
324
|
+
return new DataSet(crossJoin(this.#data, other.data))
|
|
296
325
|
}
|
|
297
326
|
semiJoin(other, condition) {
|
|
298
|
-
return new DataSet(semiJoin(this
|
|
327
|
+
return new DataSet(semiJoin(this.#data, other.data, condition))
|
|
299
328
|
}
|
|
300
329
|
antiJoin(other, condition) {
|
|
301
|
-
return new DataSet(antiJoin(this
|
|
330
|
+
return new DataSet(antiJoin(this.#data, other.data, condition))
|
|
302
331
|
}
|
|
303
332
|
nestedJoin(other, condition) {
|
|
304
|
-
return new DataSet(nestedJoin(this
|
|
333
|
+
return new DataSet(nestedJoin(this.#data, other.data, condition, this.#config.children))
|
|
305
334
|
}
|
|
306
335
|
}
|
|
307
336
|
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ReactiveDataSet — Svelte 5 rune-based reactive wrapper for dataset operations.
|
|
3
|
+
*
|
|
4
|
+
* Use this in Svelte 5 components when you need the pipeline to auto-recompute
|
|
5
|
+
* when source data changes (e.g. live search, CRUD lists).
|
|
6
|
+
*
|
|
7
|
+
* Unlike `DataSet`, which is imperatively pipeline-driven, `ReactiveDataSet` holds
|
|
8
|
+
* reactive configuration ($state) and exposes a `rows` getter backed by $derived —
|
|
9
|
+
* so templates can bind to `ds.rows` directly without calling `.select()`.
|
|
10
|
+
*
|
|
11
|
+
* File must remain `.svelte.js` for Svelte 5 rune compilation.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { executeFused } from './pipeline.js'
|
|
15
|
+
import { deriveColumnDefs } from './infer.js'
|
|
16
|
+
|
|
17
|
+
export class ReactiveDataSet {
|
|
18
|
+
/** @type {any[]} */
|
|
19
|
+
#source = $state([])
|
|
20
|
+
|
|
21
|
+
/** @type {Function|null} */
|
|
22
|
+
#filter = $state(null)
|
|
23
|
+
|
|
24
|
+
/** @type {Array<{fn: Function}>} */
|
|
25
|
+
#transforms = $state([])
|
|
26
|
+
|
|
27
|
+
/** @type {string[]} */
|
|
28
|
+
#sortFields = $state([])
|
|
29
|
+
|
|
30
|
+
/** @type {Array<Object>} Per-column overrides merged into auto-derived column defs. */
|
|
31
|
+
#columnEnhancements = $state([])
|
|
32
|
+
|
|
33
|
+
/** @type {Object} Options forwarded to deriveColumnDefs (language, scanMode, etc.) */
|
|
34
|
+
#columnOptions = $state({})
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @param {any[]} data - Initial source rows.
|
|
38
|
+
*/
|
|
39
|
+
constructor(data = []) {
|
|
40
|
+
this.#source = data
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Reactive computed result of filter + transforms + sort.
|
|
45
|
+
* Bind to this in templates instead of calling `.select()`.
|
|
46
|
+
*/
|
|
47
|
+
get rows() {
|
|
48
|
+
const ops = []
|
|
49
|
+
if (this.#filter) ops.push({ type: 'filter', fn: this.#filter })
|
|
50
|
+
for (const t of this.#transforms) ops.push({ type: 'map', fn: t.fn })
|
|
51
|
+
if (this.#sortFields.length > 0) ops.push({ type: 'sort', fields: this.#sortFields })
|
|
52
|
+
return executeFused(this.#source, ops)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Reactive derived column definitions: name, type, scale (discrete|continuous),
|
|
57
|
+
* sortable, filterable, fields, formatter — merged with any `withColumns` overrides.
|
|
58
|
+
*
|
|
59
|
+
* Re-computes when source data or column enhancements change.
|
|
60
|
+
* Bind to this in templates for Table / TreeTable column specs.
|
|
61
|
+
*/
|
|
62
|
+
get columns() {
|
|
63
|
+
return deriveColumnDefs(this.#source, {
|
|
64
|
+
...this.#columnOptions,
|
|
65
|
+
enhancements: this.#columnEnhancements
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ── Configuration (chainable) ────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Set the filter predicate. Replaces any previous filter.
|
|
73
|
+
* @param {Function|null} fn
|
|
74
|
+
*/
|
|
75
|
+
where(fn) {
|
|
76
|
+
this.#filter = fn
|
|
77
|
+
return this
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Append a row transform. Each call to `apply` adds a new stage.
|
|
82
|
+
* @param {Function} fn
|
|
83
|
+
*/
|
|
84
|
+
apply(fn) {
|
|
85
|
+
this.#transforms = [...this.#transforms, { fn }]
|
|
86
|
+
return this
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Set sort fields. Replaces any previous sort.
|
|
91
|
+
* @param {...string} fields
|
|
92
|
+
*/
|
|
93
|
+
sortBy(...fields) {
|
|
94
|
+
this.#sortFields = fields
|
|
95
|
+
return this
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Set per-column enhancements / overrides for the reactive `columns` property.
|
|
100
|
+
*
|
|
101
|
+
* Each entry is matched by `name` and merged into the auto-derived column def.
|
|
102
|
+
* Unknown names are appended as synthetic columns (useful for action columns).
|
|
103
|
+
*
|
|
104
|
+
* @param {Array<Object>} enhancements - e.g. [{ name: 'revenue', label: 'Revenue ($)', scale: 'continuous' }]
|
|
105
|
+
* @param {Object} [options] - Options forwarded to deriveColumnDefs (language, scanMode).
|
|
106
|
+
*/
|
|
107
|
+
withColumns(enhancements, options = {}) {
|
|
108
|
+
this.#columnEnhancements = enhancements
|
|
109
|
+
this.#columnOptions = options
|
|
110
|
+
return this
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Clear the current filter. */
|
|
114
|
+
clearFilter() {
|
|
115
|
+
this.#filter = null
|
|
116
|
+
return this
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Clear all transforms. */
|
|
120
|
+
clearTransforms() {
|
|
121
|
+
this.#transforms = []
|
|
122
|
+
return this
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ── Data mutation ────────────────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Replace the entire source dataset.
|
|
129
|
+
* @param {any[]} rows
|
|
130
|
+
*/
|
|
131
|
+
setData(rows) {
|
|
132
|
+
this.#source = rows
|
|
133
|
+
return this
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Append a single row to the source.
|
|
138
|
+
* @param {any} row
|
|
139
|
+
*/
|
|
140
|
+
push(row) {
|
|
141
|
+
this.#source = [...this.#source, row]
|
|
142
|
+
return this
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Remove rows matching the predicate from the source.
|
|
147
|
+
* @param {Function} predicate
|
|
148
|
+
*/
|
|
149
|
+
remove(predicate) {
|
|
150
|
+
this.#source = this.#source.filter((r) => !predicate(r))
|
|
151
|
+
return this
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Update rows matching the predicate with patch (object or function).
|
|
156
|
+
* @param {Function} predicate
|
|
157
|
+
* @param {Object|Function} patch
|
|
158
|
+
*/
|
|
159
|
+
update(predicate, patch) {
|
|
160
|
+
this.#source = this.#source.map((row) => {
|
|
161
|
+
if (!predicate(row)) return row
|
|
162
|
+
return typeof patch === 'function' ? patch(row) : { ...row, ...patch }
|
|
163
|
+
})
|
|
164
|
+
return this
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Return a plain snapshot of the current reactive result (non-reactive copy). */
|
|
168
|
+
snapshot() {
|
|
169
|
+
return [...this.rows]
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Factory function for `ReactiveDataSet`.
|
|
175
|
+
*
|
|
176
|
+
* @param {any[]} [data=[]] - Initial source rows.
|
|
177
|
+
* @returns {ReactiveDataSet}
|
|
178
|
+
*/
|
|
179
|
+
export function reactiveDataset(data = []) {
|
|
180
|
+
return new ReactiveDataSet(data)
|
|
181
|
+
}
|
package/src/formatter.js
CHANGED
package/src/index.js
CHANGED
|
@@ -6,6 +6,14 @@ export { model } from './model'
|
|
|
6
6
|
export { innerJoin, leftJoin, rightJoin, fullJoin, crossJoin, antiJoin, semiJoin } from './join'
|
|
7
7
|
export { dataset } from './dataset'
|
|
8
8
|
export { dataview } from './view'
|
|
9
|
-
export {
|
|
9
|
+
export {
|
|
10
|
+
deriveColumns,
|
|
11
|
+
deriveMetadata,
|
|
12
|
+
deriveSortableColumn,
|
|
13
|
+
inferScale,
|
|
14
|
+
addScales,
|
|
15
|
+
deriveColumnDefs,
|
|
16
|
+
mergeColumnEnhancements
|
|
17
|
+
} from './infer'
|
|
10
18
|
export { parseFilters } from './parser'
|
|
11
19
|
export { filterData, filterObjectArray } from './filter'
|