@morscherlab/mint-sdk 1.0.39 → 1.0.41
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/{ExperimentPopover-DEzCbTqo.js → ExperimentPopover-8A4Rhffp.js} +1 -1
- package/dist/{ExperimentPopover-mzmSfAUp.js → ExperimentPopover-BbPkIFsI.js} +8 -2
- package/dist/ExperimentPopover-BbPkIFsI.js.map +1 -0
- package/dist/{ExperimentSelectorModal-Bn0Hmg07.js → ExperimentSelectorModal-B2qek_YG.js} +91 -46
- package/dist/ExperimentSelectorModal-B2qek_YG.js.map +1 -0
- package/dist/{ExperimentSelectorModal-BAIlIybO.js → ExperimentSelectorModal-BwPbQN1g.js} +1 -1
- package/dist/__tests__/components/AutoGroupModal.preview.test.d.ts +1 -0
- package/dist/__tests__/composables/autoGroup/classKey.test.d.ts +1 -0
- package/dist/__tests__/composables/autoGroup/groupTree.test.d.ts +1 -0
- package/dist/__tests__/composables/autoGroup/tokenLength.test.d.ts +1 -0
- package/dist/components/index.js +3 -3
- package/dist/{components-Cyi0IfRl.js → components-CJ2--4Ex.js} +5606 -5592
- package/dist/components-CJ2--4Ex.js.map +1 -0
- package/dist/composables/autoGroup/classKey.d.ts +1 -0
- package/dist/composables/autoGroup/index.d.ts +2 -1
- package/dist/composables/autoGroup/replicatePreGroup.d.ts +10 -12
- package/dist/composables/autoGroup/tokenLength.d.ts +17 -0
- package/dist/composables/index.js +2 -2
- package/dist/composables/useAutoGroup.d.ts +2 -0
- package/dist/{composables-CFSn4NN3.js → composables-DrE6OcZZ.js} +2 -2
- package/dist/{composables-CFSn4NN3.js.map → composables-DrE6OcZZ.js.map} +1 -1
- package/dist/index.js +5 -5
- package/dist/install.js +3 -3
- package/dist/styles.css +1497 -1453
- package/dist/types/auto-group.d.ts +19 -0
- package/dist/{useProtocolTemplates-CXP2ZosM.js → useProtocolTemplates-BbvlHoPD.js} +218 -90
- package/dist/useProtocolTemplates-BbvlHoPD.js.map +1 -0
- package/package.json +1 -1
- package/src/__tests__/components/AutoGroupModal.preview.test.ts +46 -0
- package/src/__tests__/composables/autoGroup/classKey.test.ts +25 -0
- package/src/__tests__/composables/autoGroup/fingerprint.test.ts +72 -0
- package/src/__tests__/composables/autoGroup/groupTree.test.ts +99 -0
- package/src/__tests__/composables/autoGroup/tokenLength.test.ts +85 -0
- package/src/__tests__/composables/useAutoGroup.test.ts +111 -19
- package/src/components/AutoGroupModal.vue +23 -19
- package/src/components/BaseModal.story.vue +7 -15
- package/src/components/ExperimentDataViewer.vue +1 -0
- package/src/components/ExperimentPopover.vue +6 -4
- package/src/components/ExperimentSelectorModal.vue +30 -3
- package/src/components/IconButton.story.vue +5 -0
- package/src/components/SampleSelector.vue +3 -2
- package/src/components/SampleSelectorSampleRow.vue +4 -2
- package/src/composables/autoGroup/classKey.ts +5 -2
- package/src/composables/autoGroup/columns.ts +2 -2
- package/src/composables/autoGroup/compose.ts +56 -0
- package/src/composables/autoGroup/fingerprint.ts +15 -1
- package/src/composables/autoGroup/index.ts +2 -0
- package/src/composables/autoGroup/replicatePreGroup.ts +34 -0
- package/src/composables/autoGroup/template.ts +2 -2
- package/src/composables/autoGroup/tokenLength.ts +53 -0
- package/src/composables/autoGroup/vocab.json +1 -2
- package/src/composables/useAutoGroup.ts +34 -13
- package/src/styles/components/auto-group-modal.css +7 -11
- package/src/styles/components/button.css +10 -3
- package/src/styles/components/modal.css +3 -0
- package/src/styles/components/sample-selector.css +17 -0
- package/src/styles/variables.css +8 -0
- package/src/types/auto-group.ts +19 -0
- package/dist/ExperimentPopover-mzmSfAUp.js.map +0 -1
- package/dist/ExperimentSelectorModal-Bn0Hmg07.js.map +0 -1
- package/dist/components-Cyi0IfRl.js.map +0 -1
- package/dist/useProtocolTemplates-CXP2ZosM.js.map +0 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { SampleGroup } from './components';
|
|
2
|
+
import { TreeNode } from './componentLabTypes';
|
|
2
3
|
export type OutlierAction = 'include' | 'exclude' | 'qc';
|
|
3
4
|
export type InputMode = 'paste' | 'csv' | 'experiment';
|
|
4
5
|
export interface OutlierInfo {
|
|
@@ -27,6 +28,12 @@ export interface SampleClass {
|
|
|
27
28
|
members: number[];
|
|
28
29
|
classTagPositions: number[];
|
|
29
30
|
disposition: ClassDisposition;
|
|
31
|
+
/**
|
|
32
|
+
* Token count shared by every member of this class. Set by `splitByTokenLength`
|
|
33
|
+
* after type classification so that samples with different field counts never
|
|
34
|
+
* share a schema — column index N then means the same field for every member.
|
|
35
|
+
*/
|
|
36
|
+
tokenLength?: number;
|
|
30
37
|
}
|
|
31
38
|
export type ColumnRole = 'constant' | 'factor' | 'replicate' | 'run-order' | 'numeric' | 'class-tag' | 'ignore';
|
|
32
39
|
export interface NumericParse {
|
|
@@ -64,6 +71,9 @@ export interface ColumnInfo {
|
|
|
64
71
|
export interface ClassSchema {
|
|
65
72
|
classKind: SampleClassKind;
|
|
66
73
|
subKind?: string;
|
|
74
|
+
/** Token count of the class this schema describes. Distinguishes schemas that
|
|
75
|
+
* share a (kind, subKind) but were split apart by token-length. */
|
|
76
|
+
tokenLength?: number;
|
|
67
77
|
columns: ColumnInfo[];
|
|
68
78
|
groupBy: number[];
|
|
69
79
|
replicateColumn?: number;
|
|
@@ -86,12 +96,21 @@ export interface AutoGroupResult {
|
|
|
86
96
|
excludedSamples: string[];
|
|
87
97
|
schemas?: ClassSchema[];
|
|
88
98
|
fingerprint?: SchemaFingerprint;
|
|
99
|
+
/**
|
|
100
|
+
* Experimental (disposition === 'group') classes as a nested hierarchy:
|
|
101
|
+
* one root per class, then one level per enabled groupBy column, then the
|
|
102
|
+
* sample leaves. Parent nodes carry a sample-count `badge`; leaves carry the
|
|
103
|
+
* sample name plus the detected injection number (`metadata.injection` and as
|
|
104
|
+
* the leaf badge). Renders directly in `SampleHierarchyTree`.
|
|
105
|
+
*/
|
|
106
|
+
groupTree?: TreeNode[];
|
|
89
107
|
}
|
|
90
108
|
export interface SchemaFingerprint {
|
|
91
109
|
version: 1;
|
|
92
110
|
classes: Array<{
|
|
93
111
|
kind: SampleClassKind;
|
|
94
112
|
subKind?: string;
|
|
113
|
+
tokenLength?: number;
|
|
95
114
|
columns: Array<{
|
|
96
115
|
name: string;
|
|
97
116
|
role: ColumnRole;
|
|
@@ -836,6 +836,70 @@ function useWellPlateEditor(initialState, options = {}) {
|
|
|
836
836
|
};
|
|
837
837
|
}
|
|
838
838
|
//#endregion
|
|
839
|
+
//#region src/composables/useExpansionSet.ts
|
|
840
|
+
/** Shared expansion state for trees, grouped selectors, and disclosure lists. */
|
|
841
|
+
function useExpansionSet(options = {}) {
|
|
842
|
+
const expandedIds = ref(new Set(normalizeIds(toValue(options.defaultIds))));
|
|
843
|
+
const expandedList = computed(() => [...expandedIds.value]);
|
|
844
|
+
if (options.expandAll !== void 0) watch(() => toValue(options.expandAll), (shouldExpandAll) => {
|
|
845
|
+
if (shouldExpandAll === void 0 || shouldExpandAll === null) return;
|
|
846
|
+
if (shouldExpandAll) setExpanded(normalizeIds(toValue(options.allIds)));
|
|
847
|
+
else reset();
|
|
848
|
+
}, { immediate: true });
|
|
849
|
+
if (options.allIds !== void 0) watch(() => normalizeIds(toValue(options.allIds)), (ids) => {
|
|
850
|
+
if (toValue(options.expandAll)) setExpanded(ids);
|
|
851
|
+
});
|
|
852
|
+
function isExpanded(id) {
|
|
853
|
+
return expandedIds.value.has(id);
|
|
854
|
+
}
|
|
855
|
+
function expand(id) {
|
|
856
|
+
if (expandedIds.value.has(id)) return;
|
|
857
|
+
expandedIds.value = new Set([...expandedIds.value, id]);
|
|
858
|
+
}
|
|
859
|
+
function collapse(id) {
|
|
860
|
+
if (!expandedIds.value.has(id)) return;
|
|
861
|
+
const next = new Set(expandedIds.value);
|
|
862
|
+
next.delete(id);
|
|
863
|
+
expandedIds.value = next;
|
|
864
|
+
}
|
|
865
|
+
function toggle(id) {
|
|
866
|
+
if (expandedIds.value.has(id)) {
|
|
867
|
+
collapse(id);
|
|
868
|
+
return false;
|
|
869
|
+
}
|
|
870
|
+
expand(id);
|
|
871
|
+
return true;
|
|
872
|
+
}
|
|
873
|
+
function expandMany(ids) {
|
|
874
|
+
expandedIds.value = new Set([...expandedIds.value, ...normalizeIds(ids)]);
|
|
875
|
+
}
|
|
876
|
+
function setExpanded(ids) {
|
|
877
|
+
expandedIds.value = new Set(normalizeIds(ids));
|
|
878
|
+
}
|
|
879
|
+
function collapseAll() {
|
|
880
|
+
expandedIds.value = /* @__PURE__ */ new Set();
|
|
881
|
+
}
|
|
882
|
+
function reset() {
|
|
883
|
+
setExpanded(normalizeIds(toValue(options.defaultIds)));
|
|
884
|
+
}
|
|
885
|
+
return {
|
|
886
|
+
expandedIds,
|
|
887
|
+
expandedList,
|
|
888
|
+
isExpanded,
|
|
889
|
+
expand,
|
|
890
|
+
collapse,
|
|
891
|
+
toggle,
|
|
892
|
+
expandMany,
|
|
893
|
+
setExpanded,
|
|
894
|
+
collapseAll,
|
|
895
|
+
reset
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
function normalizeIds(ids) {
|
|
899
|
+
if (!ids) return [];
|
|
900
|
+
return [...new Set(ids.filter(Boolean))];
|
|
901
|
+
}
|
|
902
|
+
//#endregion
|
|
839
903
|
//#region src/composables/autoGroup/tokenize.ts
|
|
840
904
|
var DELIMITER_CANDIDATES = [
|
|
841
905
|
"_",
|
|
@@ -873,7 +937,8 @@ function splitMulti(name, delimiter) {
|
|
|
873
937
|
//#endregion
|
|
874
938
|
//#region src/composables/autoGroup/classKey.ts
|
|
875
939
|
function classKey(c) {
|
|
876
|
-
|
|
940
|
+
const base = c.subKind ? `${c.kind}:${c.subKind}` : c.kind;
|
|
941
|
+
return c.tokenLength != null ? `${base}#${c.tokenLength}` : base;
|
|
877
942
|
}
|
|
878
943
|
var vocab_default = {
|
|
879
944
|
$schema: "./vocab.schema.json",
|
|
@@ -884,7 +949,7 @@ var vocab_default = {
|
|
|
884
949
|
"matrixVocab": "Lowercase token → canonical display name. Tokens are normalised to lowercase before lookup, so adding 'tissues' handles 'Tissues' / 'TISSUES' / 'tissues' uniformly. Multiple aliases map to one canonical (e.g. cell + cells → Cells).",
|
|
885
950
|
"ionizationModes": "Lowercase polarity tokens (pos / positive / neg / negative). Suffixed to the subKind so POS and NEG acquisitions get independent schemas.",
|
|
886
951
|
"tissueParentTokens": "Tokens that flag the immediately-following token as the organ subKind (so `tissues / kidney` becomes Biological/Tissues, tagging both positions).",
|
|
887
|
-
"replicateStripPatterns": "Regex sources stripped from each sample name during the replicate pre-grouping pass. Samples whose stripped (base) names match are treated as replicates of one another and grouped together before tokenisation. Default set covers
|
|
952
|
+
"replicateStripPatterns": "Regex sources stripped from each sample name during the replicate pre-grouping pass. Samples whose stripped (base) names match are treated as replicates of one another and grouped together before tokenisation. Default set covers _T<n>, _B<n>, and _Rep<n> markers. The trailing injection / run-order number is intentionally NOT stripped here — it survives tokenisation and is surfaced as a 'run-order' column (named 'Injection #'), then excluded from the default group key so replicates still collapse."
|
|
888
953
|
},
|
|
889
954
|
patterns: {
|
|
890
955
|
"blank": "^(blank|blk|solv?blank|matrix.?blank)$",
|
|
@@ -930,7 +995,6 @@ var vocab_default = {
|
|
|
930
995
|
},
|
|
931
996
|
tissueParentTokens: ["tissue", "tissues"],
|
|
932
997
|
replicateStripPatterns: [
|
|
933
|
-
"[_-]\\d{2,4}[A-Za-z]?$",
|
|
934
998
|
"[_-]T\\d+(?=[_-]|$)",
|
|
935
999
|
"[_-]B\\d+(?=[_-]|$)",
|
|
936
1000
|
"[_-](?:rep(?:licate)?)\\d+(?=[_-]|$)"
|
|
@@ -1223,7 +1287,7 @@ function inferColumnName(values, uniqueValues, role) {
|
|
|
1223
1287
|
if (best && best.count >= values.length / 2) return best.cat;
|
|
1224
1288
|
}
|
|
1225
1289
|
if (values.every((v) => /^\d{6}$/.test(v) || /^\d{8}$/.test(v))) return "Date";
|
|
1226
|
-
if (role === "run-order") return "
|
|
1290
|
+
if (role === "run-order") return "Injection #";
|
|
1227
1291
|
if (values.every((v) => /^(pos|neg|positive|negative)$/i.test(v))) return "Polarity";
|
|
1228
1292
|
if (values.every((v) => /^(plasma|serum|tissue|tissues|kidney|liver|tumor|urine|brain|muscle|adipose|csf|feces|faeces|cells?|media|medium|extract)$/i.test(v))) return "Matrix";
|
|
1229
1293
|
if (values.every((v) => /^(exp|expt|study)\d+$/i.test(v))) return "Experiment";
|
|
@@ -1354,6 +1418,10 @@ function composeGroups(input) {
|
|
|
1354
1418
|
const excludedSamples = [];
|
|
1355
1419
|
const metadata = [];
|
|
1356
1420
|
let colorIdx = 0;
|
|
1421
|
+
const PATH_SEP = String.fromCharCode(1);
|
|
1422
|
+
const groupTree = [];
|
|
1423
|
+
const treeIndex = /* @__PURE__ */ new Map();
|
|
1424
|
+
let treeColorIdx = 0;
|
|
1357
1425
|
for (const cls of input.classes) {
|
|
1358
1426
|
const schema = input.schemas[classKey(cls)];
|
|
1359
1427
|
if (!schema) continue;
|
|
@@ -1363,6 +1431,10 @@ function composeGroups(input) {
|
|
|
1363
1431
|
}
|
|
1364
1432
|
const groupMap = /* @__PURE__ */ new Map();
|
|
1365
1433
|
const colByIdx = new Map(schema.columns.map((c) => [c.index, c]));
|
|
1434
|
+
const buildsTree = cls.disposition === "group";
|
|
1435
|
+
const runCol = schema.columns.find((c) => c.role === "run-order");
|
|
1436
|
+
const classNodeId = classKey(cls);
|
|
1437
|
+
let classNode = null;
|
|
1366
1438
|
for (const m of cls.members) {
|
|
1367
1439
|
const tokens = input.tokenizedSamples[m];
|
|
1368
1440
|
const rowName = input.sampleNames[m];
|
|
@@ -1398,6 +1470,49 @@ function composeGroups(input) {
|
|
|
1398
1470
|
fields: fieldValues,
|
|
1399
1471
|
group: groupKey
|
|
1400
1472
|
});
|
|
1473
|
+
if (buildsTree) {
|
|
1474
|
+
if (!classNode) {
|
|
1475
|
+
classNode = {
|
|
1476
|
+
id: classNodeId,
|
|
1477
|
+
label: cls.label,
|
|
1478
|
+
badge: 0,
|
|
1479
|
+
children: [],
|
|
1480
|
+
metadata: { color: DEFAULT_COLORS[treeColorIdx++ % DEFAULT_COLORS.length] }
|
|
1481
|
+
};
|
|
1482
|
+
treeIndex.set(classNodeId, classNode);
|
|
1483
|
+
groupTree.push(classNode);
|
|
1484
|
+
}
|
|
1485
|
+
classNode.badge = classNode.badge + 1;
|
|
1486
|
+
let cursor = classNode;
|
|
1487
|
+
let pathId = classNodeId;
|
|
1488
|
+
for (const val of keyParts) {
|
|
1489
|
+
pathId += PATH_SEP + val;
|
|
1490
|
+
let node = treeIndex.get(pathId);
|
|
1491
|
+
if (!node) {
|
|
1492
|
+
node = {
|
|
1493
|
+
id: pathId,
|
|
1494
|
+
label: val,
|
|
1495
|
+
badge: 0,
|
|
1496
|
+
children: []
|
|
1497
|
+
};
|
|
1498
|
+
cursor.children.push(node);
|
|
1499
|
+
treeIndex.set(pathId, node);
|
|
1500
|
+
}
|
|
1501
|
+
node.badge = node.badge + 1;
|
|
1502
|
+
cursor = node;
|
|
1503
|
+
}
|
|
1504
|
+
const injection = runCol ? fieldValues[runCol.displayName ?? runCol.name] : void 0;
|
|
1505
|
+
const leaf = {
|
|
1506
|
+
id: pathId + PATH_SEP + rowName,
|
|
1507
|
+
label: rowName,
|
|
1508
|
+
type: "sample"
|
|
1509
|
+
};
|
|
1510
|
+
if (injection) {
|
|
1511
|
+
leaf.badge = injection;
|
|
1512
|
+
leaf.metadata = { injection };
|
|
1513
|
+
}
|
|
1514
|
+
cursor.children.push(leaf);
|
|
1515
|
+
}
|
|
1401
1516
|
}
|
|
1402
1517
|
for (const { name, samples } of groupMap.values()) {
|
|
1403
1518
|
const g = {
|
|
@@ -1415,7 +1530,8 @@ function composeGroups(input) {
|
|
|
1415
1530
|
qcGroups,
|
|
1416
1531
|
metadata,
|
|
1417
1532
|
excludedSamples,
|
|
1418
|
-
schemas: Object.values(input.schemas)
|
|
1533
|
+
schemas: Object.values(input.schemas),
|
|
1534
|
+
groupTree
|
|
1419
1535
|
};
|
|
1420
1536
|
}
|
|
1421
1537
|
//#endregion
|
|
@@ -1426,6 +1542,7 @@ function serializeFingerprint(schemas) {
|
|
|
1426
1542
|
classes: schemas.map((s) => ({
|
|
1427
1543
|
kind: s.classKind,
|
|
1428
1544
|
subKind: s.subKind,
|
|
1545
|
+
tokenLength: s.tokenLength,
|
|
1429
1546
|
columns: s.columns.map((c) => ({
|
|
1430
1547
|
name: c.displayName ?? c.name,
|
|
1431
1548
|
role: c.role ?? "factor",
|
|
@@ -1439,7 +1556,8 @@ function serializeFingerprint(schemas) {
|
|
|
1439
1556
|
}
|
|
1440
1557
|
function restoreFingerprint(fp, current) {
|
|
1441
1558
|
return fp.classes.map((snap) => {
|
|
1442
|
-
const
|
|
1559
|
+
const candidates = current.filter((c) => c.classKind === snap.kind && c.subKind === snap.subKind && (snap.tokenLength == null || c.tokenLength === snap.tokenLength));
|
|
1560
|
+
const target = candidates.find((c) => c.columns.length === snap.columns.length) ?? candidates[0];
|
|
1443
1561
|
if (!target) throw new Error(`Fingerprint class not present in current input: ${snap.kind}${snap.subKind ? "/" + snap.subKind : ""}`);
|
|
1444
1562
|
if (target.columns.length !== snap.columns.length) throw new Error(`Saved schema expects ${snap.columns.length} columns, current data has ${target.columns.length} for class ${snap.kind}${snap.subKind ? "/" + snap.subKind : ""}. column count mismatch`);
|
|
1445
1563
|
return {
|
|
@@ -1493,13 +1611,21 @@ function composeTemplate(samples, schemas, classes, options) {
|
|
|
1493
1611
|
for (const cls of classes) for (const m of cls.members) sampleToClass.set(m, cls);
|
|
1494
1612
|
const schemaByKey = /* @__PURE__ */ new Map();
|
|
1495
1613
|
for (const s of schemas) {
|
|
1496
|
-
const k =
|
|
1614
|
+
const k = classKey({
|
|
1615
|
+
kind: s.classKind,
|
|
1616
|
+
subKind: s.subKind,
|
|
1617
|
+
tokenLength: s.tokenLength
|
|
1618
|
+
});
|
|
1497
1619
|
schemaByKey.set(k, s);
|
|
1498
1620
|
}
|
|
1499
1621
|
const colByNameByKey = /* @__PURE__ */ new Map();
|
|
1500
1622
|
const groupBySetByKey = /* @__PURE__ */ new Map();
|
|
1501
1623
|
for (const s of schemas) {
|
|
1502
|
-
const k =
|
|
1624
|
+
const k = classKey({
|
|
1625
|
+
kind: s.classKind,
|
|
1626
|
+
subKind: s.subKind,
|
|
1627
|
+
tokenLength: s.tokenLength
|
|
1628
|
+
});
|
|
1503
1629
|
const m = /* @__PURE__ */ new Map();
|
|
1504
1630
|
for (const c of s.columns) m.set(c.displayName ?? c.name, c);
|
|
1505
1631
|
colByNameByKey.set(k, m);
|
|
@@ -1555,18 +1681,6 @@ function composeTemplate(samples, schemas, classes, options) {
|
|
|
1555
1681
|
}
|
|
1556
1682
|
//#endregion
|
|
1557
1683
|
//#region src/composables/autoGroup/replicatePreGroup.ts
|
|
1558
|
-
/**
|
|
1559
|
-
* Replicate pre-grouping.
|
|
1560
|
-
*
|
|
1561
|
-
* Strip the replicate / injection-number markers from each sample's name, then
|
|
1562
|
-
* collapse samples that share a base name. Those samples ARE replicates of one
|
|
1563
|
-
* another by definition; the remaining unique base names are what the rest of
|
|
1564
|
-
* the pipeline (tokenize → classify → schema → compose) should operate on.
|
|
1565
|
-
*
|
|
1566
|
-
* This pre-pass replaces the older "treat every sample as independent, then
|
|
1567
|
-
* infer replicate columns" approach. Identifying replicate-equivalence by name
|
|
1568
|
-
* before tokenisation removes a class of fragile cardinality heuristics.
|
|
1569
|
-
*/
|
|
1570
1684
|
var STRIP_PATTERNS = vocab_default.replicateStripPatterns.map((src) => new RegExp(src, "gi"));
|
|
1571
1685
|
/**
|
|
1572
1686
|
* Iteratively strip every replicate / injection-number marker from `name`.
|
|
@@ -1619,6 +1733,79 @@ function expandGroupsWithReplicates(groups, preGrouping) {
|
|
|
1619
1733
|
})
|
|
1620
1734
|
}));
|
|
1621
1735
|
}
|
|
1736
|
+
/**
|
|
1737
|
+
* Expand a group hierarchy (built over base names) back to original samples.
|
|
1738
|
+
* Each leaf node's `label` is a base name; it is replaced by one leaf per
|
|
1739
|
+
* original sample that collapsed onto it (sharing the leaf's injection badge,
|
|
1740
|
+
* since collapsed replicates have identical base tokens). Every ancestor's
|
|
1741
|
+
* `badge` (sample count) is recomputed from the expanded leaf count so the tree
|
|
1742
|
+
* agrees with the flat groups produced by `expandGroupsWithReplicates`.
|
|
1743
|
+
*/
|
|
1744
|
+
function expandTreeWithReplicates(nodes, preGrouping) {
|
|
1745
|
+
const baseToOriginals = /* @__PURE__ */ new Map();
|
|
1746
|
+
for (let i = 0; i < preGrouping.baseNames.length; i++) baseToOriginals.set(preGrouping.baseNames[i], preGrouping.membersByBase[i].map((m) => preGrouping.originalSamples[m]));
|
|
1747
|
+
const leafCount = (n) => n.children && n.children.length ? n.children.reduce((acc, c) => acc + leafCount(c), 0) : 1;
|
|
1748
|
+
const expand = (node) => {
|
|
1749
|
+
if (node.children && node.children.length) {
|
|
1750
|
+
const children = node.children.flatMap(expand);
|
|
1751
|
+
return [{
|
|
1752
|
+
...node,
|
|
1753
|
+
children,
|
|
1754
|
+
badge: children.reduce((acc, c) => acc + leafCount(c), 0)
|
|
1755
|
+
}];
|
|
1756
|
+
}
|
|
1757
|
+
const originals = baseToOriginals.get(node.label);
|
|
1758
|
+
if (!originals || originals.length === 0) return [node];
|
|
1759
|
+
return originals.map((name, k) => ({
|
|
1760
|
+
...node,
|
|
1761
|
+
id: `${node.id}${k}`,
|
|
1762
|
+
label: name
|
|
1763
|
+
}));
|
|
1764
|
+
};
|
|
1765
|
+
return nodes.flatMap(expand);
|
|
1766
|
+
}
|
|
1767
|
+
//#endregion
|
|
1768
|
+
//#region src/composables/autoGroup/tokenLength.ts
|
|
1769
|
+
/**
|
|
1770
|
+
* Token-length pre-grouping.
|
|
1771
|
+
*
|
|
1772
|
+
* After type classification (`detectClass`), members of one class can still have
|
|
1773
|
+
* different token counts — e.g. a Plasma batch where some names carry an extra
|
|
1774
|
+
* descriptor field. Splitting each class so every member shares the same token
|
|
1775
|
+
* length is the "group same token-length sample, then extract tokens inside the
|
|
1776
|
+
* group" step: it guarantees `buildClassSchema` sees a rectangular token matrix,
|
|
1777
|
+
* so column index N means the same field for every sample in the group.
|
|
1778
|
+
*
|
|
1779
|
+
* Each output class carries `tokenLength`; `classKey` folds that into the schema
|
|
1780
|
+
* key so length-split classes never collide. The label only gains a `· N fields`
|
|
1781
|
+
* suffix when a (kind, subKind) genuinely spans more than one length, keeping the
|
|
1782
|
+
* common (uniform) case visually identical to before.
|
|
1783
|
+
*/
|
|
1784
|
+
function splitByTokenLength(classes, tokenized) {
|
|
1785
|
+
const out = [];
|
|
1786
|
+
for (const cls of classes) {
|
|
1787
|
+
const byLength = /* @__PURE__ */ new Map();
|
|
1788
|
+
const lengthOrder = [];
|
|
1789
|
+
for (const m of cls.members) {
|
|
1790
|
+
const len = tokenized[m]?.length ?? 0;
|
|
1791
|
+
const bucket = byLength.get(len);
|
|
1792
|
+
if (bucket) bucket.push(m);
|
|
1793
|
+
else {
|
|
1794
|
+
byLength.set(len, [m]);
|
|
1795
|
+
lengthOrder.push(len);
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
const multipleLengths = lengthOrder.length > 1;
|
|
1799
|
+
for (const len of lengthOrder) out.push({
|
|
1800
|
+
...cls,
|
|
1801
|
+
members: byLength.get(len),
|
|
1802
|
+
tokenLength: len,
|
|
1803
|
+
classTagPositions: cls.classTagPositions.filter((p) => p < len),
|
|
1804
|
+
label: multipleLengths ? `${cls.label} · ${len} fields` : cls.label
|
|
1805
|
+
});
|
|
1806
|
+
}
|
|
1807
|
+
return out;
|
|
1808
|
+
}
|
|
1622
1809
|
//#endregion
|
|
1623
1810
|
//#region src/composables/experimentDesignData.ts
|
|
1624
1811
|
function isRecord(value) {
|
|
@@ -1861,12 +2048,15 @@ function useAutoGroup() {
|
|
|
1861
2048
|
delimiter.value = d;
|
|
1862
2049
|
tokenized.value = pre.baseNames.map((l) => splitMulti(l, d));
|
|
1863
2050
|
const hints = pre.membersByBase.map((members) => sampleTypeHints.value[members[0]]);
|
|
1864
|
-
const detected = detectClass(tokenized.value, { sampleTypeHints: hints });
|
|
2051
|
+
const detected = splitByTokenLength(detectClass(tokenized.value, { sampleTypeHints: hints }), tokenized.value);
|
|
1865
2052
|
classes.value = detected;
|
|
1866
2053
|
const newSchemas = {};
|
|
1867
2054
|
for (const cls of detected) {
|
|
1868
2055
|
const memberTokens = cls.members.map((i) => tokenized.value[i]);
|
|
1869
|
-
newSchemas[classKey(cls)] =
|
|
2056
|
+
newSchemas[classKey(cls)] = {
|
|
2057
|
+
...buildClassSchema(memberTokens, cls.kind, cls.subKind, cls.classTagPositions),
|
|
2058
|
+
tokenLength: cls.tokenLength
|
|
2059
|
+
};
|
|
1870
2060
|
}
|
|
1871
2061
|
schemas.value = newSchemas;
|
|
1872
2062
|
if (detected.length > 0) activeClassKey.value = classKey(detected[0]);
|
|
@@ -1894,7 +2084,8 @@ function useAutoGroup() {
|
|
|
1894
2084
|
...composed,
|
|
1895
2085
|
groups: expandGroupsWithReplicates(composed.groups, pre),
|
|
1896
2086
|
experimentalGroups: composed.experimentalGroups ? expandGroupsWithReplicates(composed.experimentalGroups, pre) : void 0,
|
|
1897
|
-
qcGroups: composed.qcGroups ? expandGroupsWithReplicates(composed.qcGroups, pre) : void 0
|
|
2087
|
+
qcGroups: composed.qcGroups ? expandGroupsWithReplicates(composed.qcGroups, pre) : void 0,
|
|
2088
|
+
groupTree: composed.groupTree ? expandTreeWithReplicates(composed.groupTree, pre) : composed.groupTree
|
|
1898
2089
|
};
|
|
1899
2090
|
});
|
|
1900
2091
|
const fingerprint = computed(() => serializeFingerprint(result.value.schemas ?? []));
|
|
@@ -2000,7 +2191,8 @@ function useAutoGroup() {
|
|
|
2000
2191
|
const next = {};
|
|
2001
2192
|
for (const s of restored) next[classKey({
|
|
2002
2193
|
kind: s.classKind,
|
|
2003
|
-
subKind: s.subKind
|
|
2194
|
+
subKind: s.subKind,
|
|
2195
|
+
tokenLength: s.tokenLength
|
|
2004
2196
|
})] = s;
|
|
2005
2197
|
schemas.value = next;
|
|
2006
2198
|
}
|
|
@@ -2374,70 +2566,6 @@ function getMajorGroupName(groupName, separator) {
|
|
|
2374
2566
|
return parts.length > 1 ? parts[0] : groupName;
|
|
2375
2567
|
}
|
|
2376
2568
|
//#endregion
|
|
2377
|
-
//#region src/composables/useExpansionSet.ts
|
|
2378
|
-
/** Shared expansion state for trees, grouped selectors, and disclosure lists. */
|
|
2379
|
-
function useExpansionSet(options = {}) {
|
|
2380
|
-
const expandedIds = ref(new Set(normalizeIds(toValue(options.defaultIds))));
|
|
2381
|
-
const expandedList = computed(() => [...expandedIds.value]);
|
|
2382
|
-
if (options.expandAll !== void 0) watch(() => toValue(options.expandAll), (shouldExpandAll) => {
|
|
2383
|
-
if (shouldExpandAll === void 0 || shouldExpandAll === null) return;
|
|
2384
|
-
if (shouldExpandAll) setExpanded(normalizeIds(toValue(options.allIds)));
|
|
2385
|
-
else reset();
|
|
2386
|
-
}, { immediate: true });
|
|
2387
|
-
if (options.allIds !== void 0) watch(() => normalizeIds(toValue(options.allIds)), (ids) => {
|
|
2388
|
-
if (toValue(options.expandAll)) setExpanded(ids);
|
|
2389
|
-
});
|
|
2390
|
-
function isExpanded(id) {
|
|
2391
|
-
return expandedIds.value.has(id);
|
|
2392
|
-
}
|
|
2393
|
-
function expand(id) {
|
|
2394
|
-
if (expandedIds.value.has(id)) return;
|
|
2395
|
-
expandedIds.value = new Set([...expandedIds.value, id]);
|
|
2396
|
-
}
|
|
2397
|
-
function collapse(id) {
|
|
2398
|
-
if (!expandedIds.value.has(id)) return;
|
|
2399
|
-
const next = new Set(expandedIds.value);
|
|
2400
|
-
next.delete(id);
|
|
2401
|
-
expandedIds.value = next;
|
|
2402
|
-
}
|
|
2403
|
-
function toggle(id) {
|
|
2404
|
-
if (expandedIds.value.has(id)) {
|
|
2405
|
-
collapse(id);
|
|
2406
|
-
return false;
|
|
2407
|
-
}
|
|
2408
|
-
expand(id);
|
|
2409
|
-
return true;
|
|
2410
|
-
}
|
|
2411
|
-
function expandMany(ids) {
|
|
2412
|
-
expandedIds.value = new Set([...expandedIds.value, ...normalizeIds(ids)]);
|
|
2413
|
-
}
|
|
2414
|
-
function setExpanded(ids) {
|
|
2415
|
-
expandedIds.value = new Set(normalizeIds(ids));
|
|
2416
|
-
}
|
|
2417
|
-
function collapseAll() {
|
|
2418
|
-
expandedIds.value = /* @__PURE__ */ new Set();
|
|
2419
|
-
}
|
|
2420
|
-
function reset() {
|
|
2421
|
-
setExpanded(normalizeIds(toValue(options.defaultIds)));
|
|
2422
|
-
}
|
|
2423
|
-
return {
|
|
2424
|
-
expandedIds,
|
|
2425
|
-
expandedList,
|
|
2426
|
-
isExpanded,
|
|
2427
|
-
expand,
|
|
2428
|
-
collapse,
|
|
2429
|
-
toggle,
|
|
2430
|
-
expandMany,
|
|
2431
|
-
setExpanded,
|
|
2432
|
-
collapseAll,
|
|
2433
|
-
reset
|
|
2434
|
-
};
|
|
2435
|
-
}
|
|
2436
|
-
function normalizeIds(ids) {
|
|
2437
|
-
if (!ids) return [];
|
|
2438
|
-
return [...new Set(ids.filter(Boolean))];
|
|
2439
|
-
}
|
|
2440
|
-
//#endregion
|
|
2441
2569
|
//#region src/composables/platformContextHelpers.ts
|
|
2442
2570
|
function getInjectedPlatformContext() {
|
|
2443
2571
|
if (typeof window === "undefined") return void 0;
|
|
@@ -4220,6 +4348,6 @@ function useProtocolTemplates() {
|
|
|
4220
4348
|
};
|
|
4221
4349
|
}
|
|
4222
4350
|
//#endregion
|
|
4223
|
-
export {
|
|
4351
|
+
export { extractSampleNamesFromDesignData as A, APP_EXPERIMENT_KEY as B, useSampleGroups as C, extractSamplesFromDesignData as D, hslToHex as E, useExpansionSet as F, normalizeSearchQuery as G, useTheme as H, useWellPlateEditor as I, useSortedItems as J, useTextSearch as K, useDoseCalculator as L, unwrapExperimentDesignData as M, DEFAULT_COLORS as N, useAutoGroup as O, classKey as P, DEFAULT_MOBILE_VIEWPORT_QUERY as R, resolveCurrentExperimentId as S, hexToHsl as T, useToast as U, useAppExperiment as V, candidateMatchesSearch as W, useScheduleDrag as _, useReagentSeries as a, currentExperimentFromContext as b, useBioTemplatePresetWorkspace as c, getBioTemplateComponentProps as d, toBioTemplateComponentPropsByComponent as f, useExperimentSave as g, useTemplateCollection as h, generateDilutionSeries as i, extractSampleOptionsFromDesignData as j, parseCSV as k, useBioTemplatePackWorkspace as l, useBioTemplateControls as m, DEFAULT_PRESETS as n, useGroupAssignment as o, useBioTemplateComponents as p, compareSortValues as q, DEFAULT_UNITS as r, useRackEditor as s, useProtocolTemplates as t, useBioTemplateWorkspace as u, useExperimentSamples as v, deriveShade as w, getInjectedPlatformContext as x, useExperimentData as y, useMobileSupportGate as z };
|
|
4224
4352
|
|
|
4225
|
-
//# sourceMappingURL=useProtocolTemplates-
|
|
4353
|
+
//# sourceMappingURL=useProtocolTemplates-BbvlHoPD.js.map
|