@morscherlab/mint-sdk 1.0.27 → 1.0.29
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/components/index.js +1 -1
- package/dist/{components-BT_uVU5B.js → components-CzE7YHWY.js} +3 -3
- package/dist/components-CzE7YHWY.js.map +1 -0
- package/dist/composables/autoGroup/compose.d.ts +2 -0
- package/dist/composables/index.js +2 -2
- package/dist/composables/useAutoGroup.d.ts +8 -4
- package/dist/{composables-BNP5NZte.js → composables-BLLXhxKE.js} +2 -2
- package/dist/{composables-BNP5NZte.js.map → composables-BLLXhxKE.js.map} +1 -1
- package/dist/index.js +3 -3
- package/dist/install.js +1 -1
- package/dist/{useProtocolTemplates-COIsmhsZ.js → useProtocolTemplates-CXP2ZosM.js} +63 -25
- package/dist/useProtocolTemplates-CXP2ZosM.js.map +1 -0
- package/package.json +1 -1
- package/src/__tests__/composables/useAutoGroup.test.ts +68 -0
- package/src/__tests__/composables/useAutoGroupInputSources.test.ts +20 -0
- package/src/components/AutoGroupModal.vue +1 -1
- package/src/composables/autoGroup/compose.ts +17 -6
- package/src/composables/useAutoGroup.ts +85 -27
- package/src/composables/useAutoGroupInputSources.ts +1 -1
- package/dist/components-BT_uVU5B.js.map +0 -1
- package/dist/useProtocolTemplates-COIsmhsZ.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@morscherlab/mint-sdk",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.29",
|
|
4
4
|
"description": "MINT Platform SDK — Vue 3 components, composables, and types for plugin development. MINT = Mass-spec INtegrated Toolkit.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -215,6 +215,74 @@ describe('extractSamplesFromDesignData', () => {
|
|
|
215
215
|
})
|
|
216
216
|
})
|
|
217
217
|
|
|
218
|
+
describe('useAutoGroup CSV structured hierarchy', () => {
|
|
219
|
+
it('uses CSV metadata columns as hierarchy without biological or QC class groups', () => {
|
|
220
|
+
const auto = useAutoGroup()
|
|
221
|
+
auto.inputMode.value = 'csv'
|
|
222
|
+
auto.csvData.value = parseCSV([
|
|
223
|
+
'Sample,Tissue,Treatment,sample_type',
|
|
224
|
+
'S1,Kidney,Drug,sample',
|
|
225
|
+
'S2,Liver,Drug,sample',
|
|
226
|
+
'QC_pool,,QC,qc',
|
|
227
|
+
].join('\n'))
|
|
228
|
+
auto.parseInput()
|
|
229
|
+
|
|
230
|
+
expect(auto.classes.value).toHaveLength(1)
|
|
231
|
+
expect(auto.classes.value[0].kind).toBe('unknown')
|
|
232
|
+
expect(auto.qcGroups.value).toEqual([])
|
|
233
|
+
expect(auto.groups.value.map(g => g.name).sort()).toEqual([
|
|
234
|
+
'Kidney / Drug',
|
|
235
|
+
'Liver / Drug',
|
|
236
|
+
'Outlier',
|
|
237
|
+
])
|
|
238
|
+
expect(auto.groups.value.find(g => g.name === 'Outlier')?.samples).toEqual(['QC_pool'])
|
|
239
|
+
expect(auto.groups.value.some(g => g.name.includes('Biological'))).toBe(false)
|
|
240
|
+
expect(auto.groups.value.some(g => g.name === 'QC')).toBe(false)
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
it('defaults every CSV metadata column into the group key', () => {
|
|
244
|
+
const auto = useAutoGroup()
|
|
245
|
+
auto.inputMode.value = 'csv'
|
|
246
|
+
auto.csvData.value = parseCSV([
|
|
247
|
+
'Sample,Batch,Tissue,Treatment,Timepoint',
|
|
248
|
+
'S1,B1,Kidney,Drug,5min',
|
|
249
|
+
'S2,B1,Kidney,Drug,10min',
|
|
250
|
+
].join('\n'))
|
|
251
|
+
auto.parseInput()
|
|
252
|
+
|
|
253
|
+
expect(auto.activeSchema.value?.columns.map(c => c.name)).toEqual([
|
|
254
|
+
'Batch',
|
|
255
|
+
'Tissue',
|
|
256
|
+
'Treatment',
|
|
257
|
+
'Timepoint',
|
|
258
|
+
])
|
|
259
|
+
expect(auto.activeSchema.value?.groupBy).toEqual([0, 1, 2, 3])
|
|
260
|
+
expect(auto.groups.value.map(g => g.name).sort()).toEqual([
|
|
261
|
+
'B1 / Kidney / Drug / 10min',
|
|
262
|
+
'B1 / Kidney / Drug / 5min',
|
|
263
|
+
])
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
it('limits experiment design-data grouping to provided sample names', () => {
|
|
267
|
+
const auto = useAutoGroup()
|
|
268
|
+
const loaded = auto.loadExperimentData({
|
|
269
|
+
samples: [
|
|
270
|
+
{ sample_name: 'S1', sample_type: 'sample', conditions: { Tissue: 'Kidney', Treatment: 'Control' } },
|
|
271
|
+
{ sample_name: 'S2', sample_type: 'sample', conditions: { Tissue: 'Liver', Treatment: 'Drug' } },
|
|
272
|
+
{ sample_name: 'S3', sample_type: 'sample', conditions: { Tissue: 'Brain', Treatment: 'Drug' } },
|
|
273
|
+
],
|
|
274
|
+
}, { samples: ['S2', 'MissingFromDesign'] })
|
|
275
|
+
|
|
276
|
+
expect(loaded).toBe(true)
|
|
277
|
+
expect(auto.samples.value).toEqual(['S2', 'MissingFromDesign'])
|
|
278
|
+
expect(auto.groups.value.map(g => g.name).sort()).toEqual([
|
|
279
|
+
'Liver / Drug',
|
|
280
|
+
'Outlier',
|
|
281
|
+
])
|
|
282
|
+
expect(auto.groups.value.find(g => g.name === 'Outlier')?.samples).toEqual(['MissingFromDesign'])
|
|
283
|
+
})
|
|
284
|
+
})
|
|
285
|
+
|
|
218
286
|
describe('useAutoGroup auto-disable degenerate columns', () => {
|
|
219
287
|
it('collapses unique-per-row injection numbers via replicate pre-grouping', () => {
|
|
220
288
|
// The replicate pre-grouping pass strips the trailing 3-digit run-order
|
|
@@ -71,6 +71,26 @@ describe('useAutoGroupInputSources', () => {
|
|
|
71
71
|
expect(sources.experimentLoading.value).toBe(false)
|
|
72
72
|
})
|
|
73
73
|
|
|
74
|
+
it('filters loaded experiment metadata to the provided sample subset', async () => {
|
|
75
|
+
const { autoGroup, sources } = makeSources({
|
|
76
|
+
samples: ['S2', 'MissingFromDesign'],
|
|
77
|
+
designData: {
|
|
78
|
+
samples: [
|
|
79
|
+
{ sample_name: 'S1', sample_type: 'sample', conditions: { Tissue: 'Kidney', Treatment: 'Control' } },
|
|
80
|
+
{ sample_name: 'S2', sample_type: 'sample', conditions: { Tissue: 'Liver', Treatment: 'Drug' } },
|
|
81
|
+
],
|
|
82
|
+
},
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
await sources.initializeInputSources()
|
|
86
|
+
|
|
87
|
+
expect(autoGroup.samples.value).toEqual(['S2', 'MissingFromDesign'])
|
|
88
|
+
expect(autoGroup.groups.value.map(g => g.name).sort()).toEqual([
|
|
89
|
+
'Liver / Drug',
|
|
90
|
+
'Outlier',
|
|
91
|
+
])
|
|
92
|
+
})
|
|
93
|
+
|
|
74
94
|
it('parses uploaded CSV files and can clear them back to paste mode', async () => {
|
|
75
95
|
const { autoGroup, sources } = makeSources()
|
|
76
96
|
const file = {
|
|
@@ -529,7 +529,7 @@ async function fetchExperimentData() {
|
|
|
529
529
|
return
|
|
530
530
|
}
|
|
531
531
|
|
|
532
|
-
const success = autoGroup.loadExperimentData(pluginData)
|
|
532
|
+
const success = autoGroup.loadExperimentData(pluginData, { samples: props.samples })
|
|
533
533
|
if (!success) {
|
|
534
534
|
experimentError.value = 'No sample metadata found in this experiment'
|
|
535
535
|
return
|
|
@@ -15,6 +15,8 @@ export interface ComposeInput {
|
|
|
15
15
|
sampleNames: string[]
|
|
16
16
|
schemas: Record<string, ClassSchema>
|
|
17
17
|
classes: SampleClass[]
|
|
18
|
+
prefixClassLabels?: boolean
|
|
19
|
+
missingGroupName?: string
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
/** Apply the operation pipeline to a single raw token value:
|
|
@@ -116,13 +118,22 @@ export function composeGroups(input: ComposeInput): AutoGroupResult {
|
|
|
116
118
|
// keyParts: overlay/exclude classes already use cls.label AS their
|
|
117
119
|
// single key part, so prefixing again would produce "IQC / IQC". The
|
|
118
120
|
// Unknown class also opts out — its label ("Unknown") adds no signal.
|
|
121
|
+
const hasMissingGroupPart =
|
|
122
|
+
input.missingGroupName &&
|
|
123
|
+
cls.disposition === 'group' &&
|
|
124
|
+
keyParts.some(part => part.trim().length === 0)
|
|
119
125
|
const useClassPrefix =
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
+
input.prefixClassLabels !== false &&
|
|
127
|
+
cls.disposition === 'group' &&
|
|
128
|
+
cls.kind !== 'unknown' &&
|
|
129
|
+
keyParts.length > 0
|
|
130
|
+
const groupKey = hasMissingGroupPart
|
|
131
|
+
? input.missingGroupName!
|
|
132
|
+
: useClassPrefix
|
|
133
|
+
? `${cls.label} / ${keyParts.join(' / ')}`
|
|
134
|
+
: keyParts.length > 0
|
|
135
|
+
? keyParts.join(' / ')
|
|
136
|
+
: cls.label
|
|
126
137
|
|
|
127
138
|
const bucket = groupMap.get(groupKey)
|
|
128
139
|
if (bucket) {
|
|
@@ -35,6 +35,14 @@ import { unwrapExperimentDesignData } from './experimentDesignData'
|
|
|
35
35
|
|
|
36
36
|
export { DEFAULT_COLORS } from './autoGroup'
|
|
37
37
|
|
|
38
|
+
const STRUCTURED_MISSING_GROUP = 'Outlier'
|
|
39
|
+
|
|
40
|
+
type ParsedExperimentCsvData = ParsedCsvData & { sampleTypeHints: (string | undefined)[] }
|
|
41
|
+
|
|
42
|
+
interface LoadExperimentDataOptions {
|
|
43
|
+
samples?: readonly string[]
|
|
44
|
+
}
|
|
45
|
+
|
|
38
46
|
let deprecationWarned = false
|
|
39
47
|
let outlierDeprecationWarned = false
|
|
40
48
|
function warnDeprecated(api: string) {
|
|
@@ -104,30 +112,26 @@ export function useAutoGroup() {
|
|
|
104
112
|
}
|
|
105
113
|
const tokens = csv.rows.map(r => nonSampleCols.map(col => r[col] ?? ''))
|
|
106
114
|
tokenized.value = tokens
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
})
|
|
114
|
-
const detected = detectClass(tokens, { sampleTypeHints: hints })
|
|
115
|
-
classes.value = detected
|
|
116
|
-
// Build schema per class with CSV header names
|
|
117
|
-
const newSchemas: Record<string, ClassSchema> = {}
|
|
118
|
-
for (const cls of detected) {
|
|
119
|
-
const memberTokens = cls.members.map(i => tokens[i])
|
|
120
|
-
const schema = buildClassSchema(memberTokens, cls.kind, cls.subKind, cls.classTagPositions)
|
|
121
|
-
// Override default Token N names with actual CSV header names
|
|
122
|
-
schema.columns = schema.columns.map((c, i) => ({
|
|
123
|
-
...c,
|
|
124
|
-
name: nonSampleCols[i] ?? c.name,
|
|
125
|
-
originalName: nonSampleCols[i],
|
|
126
|
-
}))
|
|
127
|
-
newSchemas[classKey(cls)] = schema
|
|
115
|
+
const structuredClass: SampleClass = {
|
|
116
|
+
kind: 'unknown',
|
|
117
|
+
label: 'Unknown',
|
|
118
|
+
members: csv.rows.map((_, i) => i),
|
|
119
|
+
classTagPositions: [],
|
|
120
|
+
disposition: 'group',
|
|
128
121
|
}
|
|
129
|
-
|
|
130
|
-
|
|
122
|
+
classes.value = [structuredClass]
|
|
123
|
+
|
|
124
|
+
const schema = buildClassSchema(tokens, 'unknown', undefined, [])
|
|
125
|
+
schema.columns = schema.columns.map((c, i) => ({
|
|
126
|
+
...c,
|
|
127
|
+
name: nonSampleCols[i] ?? c.name,
|
|
128
|
+
originalName: nonSampleCols[i],
|
|
129
|
+
}))
|
|
130
|
+
// CSV headers are already the user-authored hierarchy. Default to every
|
|
131
|
+
// metadata column so N CSV columns produce N visible grouping levels.
|
|
132
|
+
schema.groupBy = schema.columns.map(c => c.index)
|
|
133
|
+
schemas.value = { unknown: schema }
|
|
134
|
+
activeClassKey.value = 'unknown'
|
|
131
135
|
}
|
|
132
136
|
|
|
133
137
|
function parseInputFromPaste() {
|
|
@@ -193,6 +197,10 @@ export function useAutoGroup() {
|
|
|
193
197
|
sampleNames: pre ? pre.baseNames : samples.value,
|
|
194
198
|
schemas: schemas.value,
|
|
195
199
|
classes: classes.value,
|
|
200
|
+
prefixClassLabels: !(csvData.value && (inputMode.value === 'csv' || inputMode.value === 'experiment')),
|
|
201
|
+
missingGroupName: csvData.value && (inputMode.value === 'csv' || inputMode.value === 'experiment')
|
|
202
|
+
? STRUCTURED_MISSING_GROUP
|
|
203
|
+
: undefined,
|
|
196
204
|
})
|
|
197
205
|
if (!pre) return composed
|
|
198
206
|
// Expand each group's `samples` from base names back to the original
|
|
@@ -416,8 +424,11 @@ export function useAutoGroup() {
|
|
|
416
424
|
if (activeClassKey.value) setColumnName(activeClassKey.value, index, name)
|
|
417
425
|
}
|
|
418
426
|
|
|
419
|
-
function loadExperimentData(
|
|
420
|
-
|
|
427
|
+
function loadExperimentData(
|
|
428
|
+
rawData: Record<string, unknown>,
|
|
429
|
+
options: LoadExperimentDataOptions = {},
|
|
430
|
+
): boolean {
|
|
431
|
+
const parsed = extractSamplesFromDesignData(rawData, options)
|
|
421
432
|
if (!parsed) return false
|
|
422
433
|
inputMode.value = 'experiment'
|
|
423
434
|
csvData.value = parsed
|
|
@@ -460,7 +471,8 @@ export function useAutoGroup() {
|
|
|
460
471
|
|
|
461
472
|
export function extractSamplesFromDesignData(
|
|
462
473
|
rawData: Record<string, unknown>,
|
|
463
|
-
|
|
474
|
+
options: LoadExperimentDataOptions = {},
|
|
475
|
+
): ParsedExperimentCsvData | null {
|
|
464
476
|
const designData = unwrapExperimentDesignData(rawData)
|
|
465
477
|
if (!designData) return null
|
|
466
478
|
const samples = designData.samples
|
|
@@ -492,5 +504,51 @@ export function extractSamplesFromDesignData(
|
|
|
492
504
|
for (const key of allConditionKeys) row[key] = conditions[key] ?? ''
|
|
493
505
|
return row
|
|
494
506
|
})
|
|
495
|
-
return
|
|
507
|
+
return constrainParsedSamplesToInput(
|
|
508
|
+
{ columns, rows, sampleColumn: 'sample_name', delimiter: ',', sampleTypeHints },
|
|
509
|
+
options.samples,
|
|
510
|
+
)
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function constrainParsedSamplesToInput(
|
|
514
|
+
parsed: ParsedExperimentCsvData,
|
|
515
|
+
samples: readonly string[] | undefined,
|
|
516
|
+
): ParsedExperimentCsvData {
|
|
517
|
+
const requested = samples
|
|
518
|
+
?.map(sample => sample.trim())
|
|
519
|
+
.filter(sample => sample.length > 0)
|
|
520
|
+
if (!requested || requested.length === 0) return parsed
|
|
521
|
+
|
|
522
|
+
const rowBySample = new Map<string, { row: Record<string, string>; hint: string | undefined }>()
|
|
523
|
+
parsed.rows.forEach((row, index) => {
|
|
524
|
+
const sample = row[parsed.sampleColumn]
|
|
525
|
+
if (sample && !rowBySample.has(sample)) {
|
|
526
|
+
rowBySample.set(sample, { row, hint: parsed.sampleTypeHints[index] })
|
|
527
|
+
}
|
|
528
|
+
})
|
|
529
|
+
|
|
530
|
+
const metadataColumns = parsed.columns.filter(
|
|
531
|
+
column => column !== parsed.sampleColumn && column !== 'sample_type',
|
|
532
|
+
)
|
|
533
|
+
const rows: Record<string, string>[] = []
|
|
534
|
+
const sampleTypeHints: (string | undefined)[] = []
|
|
535
|
+
|
|
536
|
+
for (const sample of requested) {
|
|
537
|
+
const match = rowBySample.get(sample)
|
|
538
|
+
if (match) {
|
|
539
|
+
rows.push(match.row)
|
|
540
|
+
sampleTypeHints.push(match.hint)
|
|
541
|
+
continue
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const outlierRow: Record<string, string> = {
|
|
545
|
+
[parsed.sampleColumn]: sample,
|
|
546
|
+
sample_type: 'sample',
|
|
547
|
+
}
|
|
548
|
+
for (const column of metadataColumns) outlierRow[column] = ''
|
|
549
|
+
rows.push(outlierRow)
|
|
550
|
+
sampleTypeHints.push(undefined)
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
return { ...parsed, rows, sampleTypeHints }
|
|
496
554
|
}
|
|
@@ -64,7 +64,7 @@ export function useAutoGroupInputSources(options: UseAutoGroupInputSourcesOption
|
|
|
64
64
|
return
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
const success = options.autoGroup.loadExperimentData(pluginData)
|
|
67
|
+
const success = options.autoGroup.loadExperimentData(pluginData, { samples: options.samples.value })
|
|
68
68
|
if (!success) {
|
|
69
69
|
experimentError.value = 'No sample metadata found in this experiment'
|
|
70
70
|
return
|