@rokkit/data 1.0.0-next.147 → 1.0.0-next.149

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/benchmark/fixtures.d.ts +37 -0
  2. package/dist/benchmark/index.d.ts +2 -0
  3. package/dist/benchmark/runner.d.ts +10 -0
  4. package/dist/{constants.d.ts → src/constants.d.ts} +6 -7
  5. package/dist/{dataset.d.ts → src/dataset.d.ts} +32 -14
  6. package/dist/src/dataset.svelte.d.ts +79 -0
  7. package/dist/{index.d.ts → src/index.d.ts} +1 -1
  8. package/dist/{infer.d.ts → src/infer.d.ts} +52 -0
  9. package/dist/{model.d.ts → src/model.d.ts} +1 -1
  10. package/dist/src/pipeline.d.ts +27 -0
  11. package/dist/{rollup.d.ts → src/rollup.d.ts} +9 -12
  12. package/package.json +1 -1
  13. package/src/constants.js +9 -4
  14. package/src/dataset.js +148 -119
  15. package/src/dataset.svelte.js +181 -0
  16. package/src/formatter.js +1 -1
  17. package/src/index.js +9 -1
  18. package/src/infer.js +100 -2
  19. package/src/join.js +11 -6
  20. package/src/model.js +3 -3
  21. package/src/pipeline.js +91 -0
  22. package/src/renamer.js +8 -1
  23. package/src/rollup.js +59 -62
  24. package/src/view.js +1 -2
  25. /package/dist/{aggregators.d.ts → src/aggregators.d.ts} +0 -0
  26. /package/dist/{convert.d.ts → src/convert.d.ts} +0 -0
  27. /package/dist/{filter.d.ts → src/filter.d.ts} +0 -0
  28. /package/dist/{formatter.d.ts → src/formatter.d.ts} +0 -0
  29. /package/dist/{hierarchy.d.ts → src/hierarchy.d.ts} +0 -0
  30. /package/dist/{join.d.ts → src/join.d.ts} +0 -0
  31. /package/dist/{list.d.ts → src/list.d.ts} +0 -0
  32. /package/dist/{metadata.d.ts → src/metadata.d.ts} +0 -0
  33. /package/dist/{parser.d.ts → src/parser.d.ts} +0 -0
  34. /package/dist/{renamer.d.ts → src/renamer.d.ts} +0 -0
  35. /package/dist/{types.d.ts → src/types.d.ts} +0 -0
  36. /package/dist/{utils.d.ts → src/utils.d.ts} +0 -0
  37. /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 { deriveSortableColumn } from './infer'
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 = typeof from === 'function' ? from : Array.isArray(from) ? pick(from) : pick([from])
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: identity })
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
- * Sorts data by the specified columns.
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: omit(config.group_by),
138
- reducers: [{ field: config.children, formula: identity }]
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 = clone(config.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] = sortDataBy(group[config.children], ...config.align_by)
149
+ group[config.children] = sortDataByFields(group[config.children], ...config.align_by)
167
150
  })
168
151
  }
169
152
 
170
- const aggregatedData = aggregateData(alignedData, summaries)
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 = clone(defaultConfig)
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.#config.filter = condition
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
- usingTemplate(template) {
214
- this.#config.template = template
189
+ apply(callback) {
190
+ this.#ops.push({ type: 'map', fn: callback })
215
191
  return this
216
192
  }
217
- summarize(from, fields) {
218
- this.config = summarize(this.#config, from, fields)
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.data = renameKeys(this.data, how)
201
+ this.#data = renameKeys(this.#data, how)
224
202
  return this
225
203
  }
226
204
  drop(...fields) {
227
- const newDataSet = new DataSet(this.data.map((row) => omit(fields, row)))
228
- newDataSet.config = this.config
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
- // alter rows
232
- sortBy(...fields) {
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
- if (this.#config.filter) {
238
- this.#data = this.data.filter((row) => !this.#config.filter(row))
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
- this.data = updateRows(this.data, this.#config.filter || identity, value)
248
- this.#config.filter = undefined
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
- fillNA(value) {
252
- this.data = fillNA(this.#data, value)
241
+
242
+ // ── Aggregation ─────────────────────────────────────────────────────────
243
+
244
+ groupBy(...fields) {
245
+ this.#config.group_by = fields
253
246
  return this
254
247
  }
255
- // transform data
256
- apply(callback) {
257
- this.data = this.data.map(callback)
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
- return new DataSet(rollup(this.data, this.#config))
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.#config.filter ? this.data.filter(this.#config.filter) : this.data
265
- if (cols.length > 0) return result.map(pick(cols))
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
- // set operations
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.data.concat(other))
298
+ return new DataSet(this.#data.concat(other))
272
299
  }
273
- return new DataSet(this.data.concat(other.data))
300
+ return new DataSet(this.#data.concat(other.data))
274
301
  }
275
302
  minus(other) {
276
- return new DataSet(this.data.filter((d) => !other.find((x) => equals(x, d))))
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.data.filter((d) => other.find((x) => equals(x, d))))
306
+ return new DataSet(this.#data.filter((d) => other.find((x) => deepEqual(x, d))))
280
307
  }
281
- // joins
308
+
309
+ // ── Joins ───────────────────────────────────────────────────────────────
310
+
282
311
  innerJoin(other, condition) {
283
- return new DataSet(innerJoin(this.data, other.data, condition))
312
+ return new DataSet(innerJoin(this.#data, other.data, condition))
284
313
  }
285
314
  leftJoin(other, condition) {
286
- return new DataSet(leftJoin(this.data, other.data, condition))
315
+ return new DataSet(leftJoin(this.#data, other.data, condition))
287
316
  }
288
317
  rightJoin(other, condition) {
289
- return new DataSet(rightJoin(this.data, other.data, condition))
318
+ return new DataSet(rightJoin(this.#data, other.data, condition))
290
319
  }
291
320
  fullJoin(other, condition) {
292
- return new DataSet(fullJoin(this.data, other.data, condition))
321
+ return new DataSet(fullJoin(this.#data, other.data, condition))
293
322
  }
294
323
  crossJoin(other) {
295
- return new DataSet(crossJoin(this.data, other.data))
324
+ return new DataSet(crossJoin(this.#data, other.data))
296
325
  }
297
326
  semiJoin(other, condition) {
298
- return new DataSet(semiJoin(this.data, other.data, condition))
327
+ return new DataSet(semiJoin(this.#data, other.data, condition))
299
328
  }
300
329
  antiJoin(other, condition) {
301
- return new DataSet(antiJoin(this.data, other.data, condition))
330
+ return new DataSet(antiJoin(this.#data, other.data, condition))
302
331
  }
303
332
  nestedJoin(other, condition) {
304
- return new DataSet(nestedJoin(this.data, other.data, condition, this.#config.children))
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
@@ -1,4 +1,4 @@
1
- import { identity } from 'ramda'
1
+ const identity = (x) => x
2
2
  /**
3
3
  * Returns a currency formatter function that formats a value as a currency string.
4
4
  * @param {string} language - The IETF language tag used for locale-specific formatting.
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 { deriveColumns, deriveMetadata, deriveSortableColumn } from './infer'
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'