@rokkit/data 1.0.0-next.98 → 1.0.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.
- package/README.md +147 -35
- package/package.json +20 -29
- package/src/constants.js +9 -4
- package/src/dataset.js +242 -333
- package/src/dataset.svelte.js +181 -0
- package/src/filter.js +17 -18
- package/src/formatter.js +16 -17
- package/src/index.js +12 -0
- package/src/infer.js +179 -65
- package/src/join.js +157 -0
- package/src/metadata.js +3 -3
- package/src/model.js +48 -48
- package/src/parser.js +42 -42
- package/src/pipeline.js +97 -0
- package/src/renamer.js +48 -43
- package/src/rollup.js +71 -74
- package/src/view.js +1 -2
- package/LICENSE +0 -21
|
@@ -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/filter.js
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
import { filterOperations } from './constants'
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Checks if a row matches the given value based on the specified operator.
|
|
5
|
+
* Supports both standard and negated operators by checking the operator string
|
|
6
|
+
* for a leading '!' character, which indicates negation.
|
|
7
|
+
*
|
|
8
|
+
* @param {Object} row - The row object in which to search for the value match.
|
|
9
|
+
* @param {any} value - The value to match against the row's properties.
|
|
10
|
+
* @param {string} operator - The operator used to compare the row's properties to the value.
|
|
11
|
+
* When operator starts with '!', it negates the result of the comparison.
|
|
12
|
+
* @returns {boolean} - Returns true if the row matches the condition; otherwise, returns false.
|
|
13
|
+
*/
|
|
14
|
+
function findMatchingRow(row, value, operator) {
|
|
15
|
+
const op = operator.startsWith('!') ? operator.slice(1) : operator
|
|
16
|
+
const matched = Object.keys(row).some((key) => filterOperations[op](row[key], value))
|
|
17
|
+
|
|
18
|
+
return operator.startsWith('!') ? !matched : matched
|
|
19
|
+
}
|
|
3
20
|
/**
|
|
4
21
|
* Filters an array of objects based on a specified condition defined in the options.
|
|
5
22
|
* If a column is provided, it filters based on the value of that column. Otherwise, it filters
|
|
@@ -22,24 +39,6 @@ export function filterObjectArray(data, options) {
|
|
|
22
39
|
return data.filter((row) => findMatchingRow(row, value, operator))
|
|
23
40
|
}
|
|
24
41
|
|
|
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
42
|
/**
|
|
44
43
|
* Applies multiple filtering conditions to an array of objects. This function iterates through each filter option,
|
|
45
44
|
* successively narrowing down the data.
|
package/src/formatter.js
CHANGED
|
@@ -1,20 +1,4 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
1
|
+
const identity = (x) => x
|
|
18
2
|
/**
|
|
19
3
|
* Returns a currency formatter function that formats a value as a currency string.
|
|
20
4
|
* @param {string} language - The IETF language tag used for locale-specific formatting.
|
|
@@ -39,6 +23,21 @@ function getCurrencyFormatter(language, decimalPlaces) {
|
|
|
39
23
|
return formatter
|
|
40
24
|
}
|
|
41
25
|
|
|
26
|
+
const formatterMap = {
|
|
27
|
+
currency: getCurrencyFormatter,
|
|
28
|
+
integer: (language) => (val) => val.toLocaleString(language, { maximumFractionDigits: 0 }),
|
|
29
|
+
number: (language, decimalPlaces) => (val) =>
|
|
30
|
+
val.toLocaleString(language, {
|
|
31
|
+
minimumFractionDigits: decimalPlaces,
|
|
32
|
+
maximumFractionDigits: decimalPlaces
|
|
33
|
+
}),
|
|
34
|
+
date: (language) => (val) => val.toLocaleDateString(language),
|
|
35
|
+
time: (language) => (val) => val.toLocaleTimeString(language),
|
|
36
|
+
object: () => JSON.stringify,
|
|
37
|
+
array: () => JSON.stringify, // Combined formatter for 'object' and 'array'
|
|
38
|
+
ellipsis: () => () => '...' // Static formatter, no need for parameters
|
|
39
|
+
}
|
|
40
|
+
|
|
42
41
|
/**
|
|
43
42
|
* Creates a formatter function that formats a value according to the specified type and optional locale settings.
|
|
44
43
|
* Supported types include 'default' (no formatting), 'integer', 'number', 'date', 'time',, 'object', 'array', 'currency' & 'ellipsis''
|
package/src/index.js
CHANGED
|
@@ -3,5 +3,17 @@ export * from './types'
|
|
|
3
3
|
export { typeOf } from './utils'
|
|
4
4
|
export { renamer } from './renamer'
|
|
5
5
|
export { model } from './model'
|
|
6
|
+
export { innerJoin, leftJoin, rightJoin, fullJoin, crossJoin, antiJoin, semiJoin } from './join'
|
|
6
7
|
export { dataset } from './dataset'
|
|
7
8
|
export { dataview } from './view'
|
|
9
|
+
export {
|
|
10
|
+
deriveColumns,
|
|
11
|
+
deriveMetadata,
|
|
12
|
+
deriveSortableColumn,
|
|
13
|
+
inferScale,
|
|
14
|
+
addScales,
|
|
15
|
+
deriveColumnDefs,
|
|
16
|
+
mergeColumnEnhancements
|
|
17
|
+
} from './infer'
|
|
18
|
+
export { parseFilters } from './parser'
|
|
19
|
+
export { filterData, filterObjectArray } from './filter'
|
package/src/infer.js
CHANGED
|
@@ -1,33 +1,35 @@
|
|
|
1
1
|
import { ascending, descending } from 'd3-array'
|
|
2
2
|
import { compact } from '@rokkit/core'
|
|
3
|
-
import { omit } from 'ramda'
|
|
4
3
|
import { createFormatter } from './formatter'
|
|
5
4
|
import { defaultViewOptions, defaultActionOrder } from './constants'
|
|
6
5
|
import { typeOf } from './utils'
|
|
7
6
|
import { deriveColumnProperties } from './metadata'
|
|
8
7
|
|
|
9
8
|
/**
|
|
10
|
-
*
|
|
9
|
+
* Validates an array to ensure it can be used to create a sorting configuration.
|
|
11
10
|
*
|
|
12
|
-
* @param {
|
|
13
|
-
* @
|
|
14
|
-
* @throws {Error} Throws an error if the value type is not supported or the value format is invalid.
|
|
11
|
+
* @param {Array} value - The array to validate.
|
|
12
|
+
* @throws {Error} Throws an error if the array does not correspond to a [string, boolean] pair.
|
|
15
13
|
*/
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const configGenerators = {
|
|
20
|
-
array: arrayConfigGenerator,
|
|
21
|
-
object: objectConfigGenerator,
|
|
22
|
-
string: stringConfigGenerator
|
|
14
|
+
function arrayValidator(value) {
|
|
15
|
+
if (value.length !== 2 || typeof value[0] !== 'string' || typeof value[1] !== 'boolean') {
|
|
16
|
+
throw new Error('Array should be a pair of [string, boolean]')
|
|
23
17
|
}
|
|
18
|
+
}
|
|
24
19
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Validates an object to ensure it has the necessary properties for creating a sorting configuration.
|
|
22
|
+
*
|
|
23
|
+
* @param {Object} value - The object to validate.
|
|
24
|
+
* @throws {Error} Throws an error if the object does not have a 'name' property or has an invalid 'sorter'.
|
|
25
|
+
*/
|
|
26
|
+
function objectValidator(value) {
|
|
27
|
+
if (typeof value.name !== 'string') {
|
|
28
|
+
throw new Error('The property "name" is required and must be a string')
|
|
29
|
+
}
|
|
30
|
+
if (value.sorter !== undefined && typeof value.sorter !== 'function') {
|
|
31
|
+
throw new Error('Sorter should be a function')
|
|
28
32
|
}
|
|
29
|
-
|
|
30
|
-
return configGenerator(value)
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
/**
|
|
@@ -63,32 +65,28 @@ function stringConfigGenerator(value) {
|
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
/**
|
|
66
|
-
*
|
|
68
|
+
* Derives the sorting information for a column based on the type of the input value.
|
|
67
69
|
*
|
|
68
|
-
* @param {
|
|
69
|
-
* @
|
|
70
|
+
* @param {import('./types').SortableColumn} value - The value that determines the sorting behavior of the column.
|
|
71
|
+
* @returns {import('./types').ColumnSorter} An object containing the 'name' of the column and a 'sorter' function.
|
|
72
|
+
* @throws {Error} Throws an error if the value type is not supported or the value format is invalid.
|
|
70
73
|
*/
|
|
71
|
-
function
|
|
72
|
-
|
|
73
|
-
throw new Error('Array should be a pair of [string, boolean]')
|
|
74
|
-
}
|
|
75
|
-
}
|
|
74
|
+
export function deriveSortableColumn(value) {
|
|
75
|
+
const type = typeOf(value)
|
|
76
76
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
* @throws {Error} Throws an error if the object does not have a 'name' property or has an invalid 'sorter'.
|
|
82
|
-
*/
|
|
83
|
-
function objectValidator(value) {
|
|
84
|
-
if (typeof value.name !== 'string') {
|
|
85
|
-
throw new Error('The property "name" is required and must be a string')
|
|
77
|
+
const configGenerators = {
|
|
78
|
+
array: arrayConfigGenerator,
|
|
79
|
+
object: objectConfigGenerator,
|
|
80
|
+
string: stringConfigGenerator
|
|
86
81
|
}
|
|
87
|
-
|
|
88
|
-
|
|
82
|
+
|
|
83
|
+
const configGenerator = configGenerators[type]
|
|
84
|
+
if (!configGenerator) {
|
|
85
|
+
throw new Error(`Invalid value type: ${type}`)
|
|
89
86
|
}
|
|
90
|
-
}
|
|
91
87
|
|
|
88
|
+
return configGenerator(value)
|
|
89
|
+
}
|
|
92
90
|
/**
|
|
93
91
|
* Creates a deep scan sample object that contains a union of all keys from all
|
|
94
92
|
* objects in the array, pulling the values from the last item that contains the key.
|
|
@@ -96,11 +94,13 @@ function objectValidator(value) {
|
|
|
96
94
|
* @param {Array<Object>} data - An array of objects to deep scan and collect sample values.
|
|
97
95
|
* @returns {Object} A sample object consolidating all unique keys with their latest non-null values.
|
|
98
96
|
*/
|
|
99
|
-
export function
|
|
97
|
+
export function getSample(data, deepScan = true) {
|
|
98
|
+
if (data.length === 0) return {}
|
|
99
|
+
if (!deepScan) return data[0]
|
|
100
100
|
return data.reduce(
|
|
101
101
|
(acc, cur) => ({
|
|
102
102
|
...acc,
|
|
103
|
-
...compact(
|
|
103
|
+
...compact(Object.fromEntries(Object.entries(cur).filter(([k]) => !(k in acc))))
|
|
104
104
|
}),
|
|
105
105
|
{}
|
|
106
106
|
)
|
|
@@ -117,7 +117,7 @@ export function deriveColumns(data, options) {
|
|
|
117
117
|
if (data.length === 0) return []
|
|
118
118
|
|
|
119
119
|
const { scanMode } = { ...defaultViewOptions, ...options }
|
|
120
|
-
const sample = scanMode === 'fast' ? data[0] :
|
|
120
|
+
const sample = scanMode === 'fast' ? data[0] : getSample(data)
|
|
121
121
|
|
|
122
122
|
return deriveColumnProperties(sample, options)
|
|
123
123
|
}
|
|
@@ -178,31 +178,6 @@ export function inferDataType(values) {
|
|
|
178
178
|
return type
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
-
/**
|
|
182
|
-
* Derives column metadata from the data to be used in a tabular component.
|
|
183
|
-
*
|
|
184
|
-
* @param {Array} data - The data to derive column metadata from.
|
|
185
|
-
* @returns {Array<import('./types').ColumnMetadata>} - The derived column metadata.
|
|
186
|
-
*/
|
|
187
|
-
export function deriveMetadata(dataArray, options = {}) {
|
|
188
|
-
let { columns = [] } = options
|
|
189
|
-
if (dataArray.length === 0) return columns
|
|
190
|
-
const { actions, language } = { ...defaultViewOptions, ...options }
|
|
191
|
-
|
|
192
|
-
if (columns.length === 0) {
|
|
193
|
-
columns = deriveColumns(dataArray, options)
|
|
194
|
-
} else {
|
|
195
|
-
columns.forEach((col) => {
|
|
196
|
-
// if (col.sortable && !col.order) col.order = 'none'
|
|
197
|
-
if (!col.fields) col.fields = { text: col.name }
|
|
198
|
-
})
|
|
199
|
-
}
|
|
200
|
-
columns = addFormatters(columns, language)
|
|
201
|
-
if (actions.length > 0) columns = deriveActions(columns, actions)
|
|
202
|
-
|
|
203
|
-
return columns
|
|
204
|
-
}
|
|
205
|
-
|
|
206
181
|
/**
|
|
207
182
|
* Adds a formatter function to each column in the array based on the column type. If a formatter
|
|
208
183
|
* function is already present, it will not be overwritten. The formatter is created based on the
|
|
@@ -261,3 +236,142 @@ export function deriveActions(columns, input) {
|
|
|
261
236
|
}))
|
|
262
237
|
return [...actionColumns, ...columns]
|
|
263
238
|
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Derives column metadata from the data to be used in a tabular component.
|
|
242
|
+
* @param {Array<import('./types').ColumnMetadata>} columns
|
|
243
|
+
* @param {Array} dataArray - The data to derive column metadata from.
|
|
244
|
+
* @param {Object} [options] - The options to use when deriving column metadata.
|
|
245
|
+
* @returns {Array<import('./types').ColumnMetadata>} - The derived column metadata.
|
|
246
|
+
*/
|
|
247
|
+
function deriveColumnMetadata(columns, dataArray, options) {
|
|
248
|
+
if (columns.length === 0) {
|
|
249
|
+
columns = deriveColumns(dataArray, options)
|
|
250
|
+
} else {
|
|
251
|
+
columns.forEach((col) => {
|
|
252
|
+
// if (col.sortable && !col.order) col.order = 'none'
|
|
253
|
+
if (!col.fields) col.fields = { text: col.name }
|
|
254
|
+
})
|
|
255
|
+
}
|
|
256
|
+
return columns
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Derives column metadata from the data to be used in a tabular component.
|
|
260
|
+
*
|
|
261
|
+
* @param {Array} data - The data to derive column metadata from.
|
|
262
|
+
* @param {Object} [options] - The options to use when deriving column metadata.
|
|
263
|
+
* @returns {Array<import('./types').ColumnMetadata>} - The derived column metadata.
|
|
264
|
+
*/
|
|
265
|
+
export function deriveMetadata(dataArray, options = {}) {
|
|
266
|
+
let { columns = [] } = options
|
|
267
|
+
if (dataArray.length === 0) return columns
|
|
268
|
+
const { actions, language } = { ...defaultViewOptions, ...options }
|
|
269
|
+
|
|
270
|
+
columns = deriveColumnMetadata(columns, dataArray, options)
|
|
271
|
+
columns = addFormatters(columns, language)
|
|
272
|
+
if (actions.length > 0) columns = deriveActions(columns, actions)
|
|
273
|
+
|
|
274
|
+
return columns
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ── Scale inference ──────────────────────────────────────────────────────────
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Infers whether a column's values are discrete (categorical) or continuous (quantitative).
|
|
281
|
+
*
|
|
282
|
+
* Rules:
|
|
283
|
+
* - string, boolean → always discrete
|
|
284
|
+
* - date, number (float) → always continuous
|
|
285
|
+
* - integer → discrete when ≤20 unique values OR unique ratio < 5 %,
|
|
286
|
+
* continuous otherwise (e.g. large ID column)
|
|
287
|
+
* - anything else → discrete (safe default for unknown types)
|
|
288
|
+
*
|
|
289
|
+
* @param {any[]} values - All values for the column (may include nulls).
|
|
290
|
+
* @returns {'discrete'|'continuous'}
|
|
291
|
+
*/
|
|
292
|
+
function inferIntegerScale(nonNull) {
|
|
293
|
+
const unique = new Set(nonNull).size
|
|
294
|
+
return unique <= 20 || unique / nonNull.length < 0.05 ? 'discrete' : 'continuous'
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const SCALE_BY_TYPE = {
|
|
298
|
+
string: () => 'discrete',
|
|
299
|
+
boolean: () => 'discrete',
|
|
300
|
+
date: () => 'continuous',
|
|
301
|
+
number: () => 'continuous',
|
|
302
|
+
integer: (nonNull) => inferIntegerScale(nonNull)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export function inferScale(values) {
|
|
306
|
+
const nonNull = values.filter((v) => v !== null && v !== undefined)
|
|
307
|
+
if (nonNull.length === 0) return 'discrete'
|
|
308
|
+
const type = typeOf(nonNull[0])
|
|
309
|
+
return (SCALE_BY_TYPE[type] ?? (() => 'discrete'))(nonNull)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Adds a `scale` property to each column definition.
|
|
314
|
+
*
|
|
315
|
+
* @param {Array<Object>} columns - Column definitions (from deriveColumns / deriveMetadata).
|
|
316
|
+
* @param {any[]} data - Source rows.
|
|
317
|
+
* @returns {Array<Object>} Column definitions with `scale` added.
|
|
318
|
+
*/
|
|
319
|
+
export function addScales(columns, data) {
|
|
320
|
+
return columns.map((col) => ({
|
|
321
|
+
...col,
|
|
322
|
+
scale: inferScale(data.map((r) => r[col.name]))
|
|
323
|
+
}))
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Merges user-provided enhancements into auto-derived column definitions.
|
|
328
|
+
* Enhancements are matched by `name` and merged (user values win).
|
|
329
|
+
* Enhancements for unknown column names are appended as synthetic columns.
|
|
330
|
+
*
|
|
331
|
+
* @param {Array<Object>} derived - Auto-derived column definitions.
|
|
332
|
+
* @param {Array<Object>} enhancements - User-provided overrides/additions.
|
|
333
|
+
* @returns {Array<Object>} Merged column definitions.
|
|
334
|
+
*/
|
|
335
|
+
export function mergeColumnEnhancements(derived, enhancements) {
|
|
336
|
+
const enhMap = new Map(enhancements.map((e) => [e.name, e]))
|
|
337
|
+
const result = derived.map((col) => {
|
|
338
|
+
const enh = enhMap.get(col.name)
|
|
339
|
+
return enh ? { ...col, ...enh } : col
|
|
340
|
+
})
|
|
341
|
+
const derivedNames = new Set(derived.map((c) => c.name))
|
|
342
|
+
for (const e of enhancements) {
|
|
343
|
+
if (!derivedNames.has(e.name)) result.push(e)
|
|
344
|
+
}
|
|
345
|
+
return result
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Full column definition pipeline: derive → format → scale → merge enhancements.
|
|
350
|
+
*
|
|
351
|
+
* Produces column descriptors suitable for use in Table, TreeTable, and chart
|
|
352
|
+
* components. Each column gets: name, type, scale, sortable, filterable, fields,
|
|
353
|
+
* formatter — plus any user-supplied properties from `enhancements`.
|
|
354
|
+
*
|
|
355
|
+
* @param {any[]} data - Source rows.
|
|
356
|
+
* @param {Object} [options] - Options forwarded to deriveColumns / addFormatters.
|
|
357
|
+
* @param {Array<Object>} [options.enhancements=[]] - Per-column overrides keyed by name.
|
|
358
|
+
* Use to override scale, add a label, attach a custom formatter, etc.
|
|
359
|
+
* @param {string} [options.language='en-US'] - Locale for formatters.
|
|
360
|
+
* @param {string} [options.scanMode='fast'] - 'fast' (first row) or 'deep' (all rows).
|
|
361
|
+
* @returns {Array<Object>} Column definitions with scale and formatters.
|
|
362
|
+
*/
|
|
363
|
+
export function deriveColumnDefs(data, options = {}) {
|
|
364
|
+
const { enhancements = [], language, scanMode } = { ...defaultViewOptions, ...options }
|
|
365
|
+
|
|
366
|
+
if (data.length === 0) return enhancements
|
|
367
|
+
|
|
368
|
+
let columns = deriveColumns(data, { scanMode })
|
|
369
|
+
columns = addFormatters(columns, language)
|
|
370
|
+
columns = addScales(columns, data)
|
|
371
|
+
|
|
372
|
+
if (enhancements.length > 0) {
|
|
373
|
+
columns = mergeColumnEnhancements(columns, enhancements)
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return columns
|
|
377
|
+
}
|