@morscherlab/mint-sdk 1.3.0-beta.9 → 1.3.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/dist/__tests__/components/AutoGroupModal.anatomy.test.d.ts +1 -0
- package/dist/__tests__/components/AutoGroupModal.wiring.test.d.ts +1 -0
- package/dist/__tests__/composables/autoGroup/fieldEdits.test.d.ts +1 -0
- package/dist/__tests__/composables/autoGroup/golden.test.d.ts +1 -0
- package/dist/components/AutoGroupModal.adapter.d.ts +27 -4
- package/dist/components/SmartGroup.types.d.ts +22 -0
- package/dist/components/SmartGroupFieldRecipe.groups.d.ts +10 -2
- package/dist/components/SmartGroupFieldRecipe.vue.d.ts +3 -0
- package/dist/components/index.js +1 -1
- package/dist/{components-GDDvof8D.js → components-bbanLd1u.js} +510 -287
- package/dist/components-bbanLd1u.js.map +1 -0
- package/dist/composables/autoGroup/classify.d.ts +7 -1
- package/dist/composables/autoGroup/columns.d.ts +16 -1
- package/dist/composables/autoGroup/index.d.ts +5 -7
- package/dist/composables/autoGroup/tokenize.d.ts +36 -1
- package/dist/composables/index.js +2 -2
- package/dist/composables/useAutoGroup.d.ts +13 -2
- package/dist/{composables-D6uqp0UF.js → composables-BbHozlA7.js} +2 -2
- package/dist/{composables-D6uqp0UF.js.map → composables-BbHozlA7.js.map} +1 -1
- package/dist/index.js +3 -3
- package/dist/install.js +1 -1
- package/dist/permissions.js +2 -2
- package/dist/permissions.js.map +1 -1
- package/dist/styles.css +129 -96
- package/dist/types/auto-group.d.ts +1 -1
- package/dist/{validation-Dmp9Q5W0.js → validation-CukfLjy-.js} +323 -187
- package/dist/validation-CukfLjy-.js.map +1 -0
- package/package.json +2 -2
- package/src/__tests__/components/AppTopBarPageSelector.test.ts +4 -4
- package/src/__tests__/components/AutoGroupModal.anatomy.test.ts +86 -0
- package/src/__tests__/components/AutoGroupModal.wiring.test.ts +53 -0
- package/src/__tests__/components/SampleSelector.test.ts +14 -16
- package/src/__tests__/components/SmartGroupFieldRecipe.groups.test.ts +23 -0
- package/src/__tests__/components/SmartGroupFieldRecipe.test.ts +176 -0
- package/src/__tests__/composables/autoGroup/columns.test.ts +100 -11
- package/src/__tests__/composables/autoGroup/fieldEdits.test.ts +74 -0
- package/src/__tests__/composables/autoGroup/fingerprint.test.ts +9 -0
- package/src/__tests__/composables/autoGroup/golden.test.ts +187 -0
- package/src/__tests__/composables/autoGroup/groupByOrder.test.ts +7 -7
- package/src/__tests__/composables/autoGroup/tokenLength.test.ts +7 -0
- package/src/__tests__/composables/autoGroup/tokenize.test.ts +78 -1
- package/src/__tests__/composables/useAutoGroup.test.ts +2 -1
- package/src/__tests__/fixtures/auto-group/share-exp021.txt +103 -0
- package/src/__tests__/fixtures/auto-group/share-exp031.txt +39 -0
- package/src/__tests__/fixtures/auto-group/share-exp114.txt +123 -0
- package/src/__tests__/fixtures/auto-group/share-exp214.txt +63 -0
- package/src/__tests__/utils/permissions.test.ts +15 -0
- package/src/components/AutoGroupModal.adapter.ts +80 -9
- package/src/components/AutoGroupModal.vue +41 -8
- package/src/components/SmartGroup.types.ts +23 -0
- package/src/components/SmartGroupFieldRecipe.groups.ts +14 -2
- package/src/components/SmartGroupFieldRecipe.vue +289 -169
- package/src/components/internal/AppTopBarPageSelectorInternal.vue +3 -6
- package/src/composables/autoGroup/classify.ts +26 -7
- package/src/composables/autoGroup/columns.ts +144 -36
- package/src/composables/autoGroup/compose.ts +8 -10
- package/src/composables/autoGroup/fingerprint.ts +1 -1
- package/src/composables/autoGroup/index.ts +5 -12
- package/src/composables/autoGroup/tokenLength.ts +8 -0
- package/src/composables/autoGroup/tokenize.ts +148 -1
- package/src/composables/autoGroup/vocab.json +5 -11
- package/src/composables/useAutoGroup.ts +92 -79
- package/src/permissions.ts +3 -2
- package/src/styles/components/app-page-selector.css +58 -14
- package/src/styles/components/smart-group.css +71 -82
- package/src/types/auto-group.ts +1 -0
- package/dist/components-GDDvof8D.js.map +0 -1
- package/dist/composables/autoGroup/replicatePreGroup.d.ts +0 -36
- package/dist/validation-Dmp9Q5W0.js.map +0 -1
- package/src/composables/autoGroup/replicatePreGroup.ts +0 -124
|
@@ -3743,41 +3743,6 @@ function useRackEditor(initialRacks, options) {
|
|
|
3743
3743
|
};
|
|
3744
3744
|
}
|
|
3745
3745
|
//#endregion
|
|
3746
|
-
//#region src/composables/autoGroup/tokenize.ts
|
|
3747
|
-
var DELIMITER_CANDIDATES = [
|
|
3748
|
-
"_",
|
|
3749
|
-
"-",
|
|
3750
|
-
"."
|
|
3751
|
-
];
|
|
3752
|
-
/** Pick the delimiter whose split produces the most consistent field count across `lines`. */
|
|
3753
|
-
function pickPrimaryDelimiter(lines) {
|
|
3754
|
-
if (lines.length === 0) return "_";
|
|
3755
|
-
let best = "_";
|
|
3756
|
-
let bestScore = -1;
|
|
3757
|
-
for (const candidate of DELIMITER_CANDIDATES) {
|
|
3758
|
-
const counts = lines.map((l) => l.split(candidate).length);
|
|
3759
|
-
const frequency = /* @__PURE__ */ new Map();
|
|
3760
|
-
for (const c of counts) frequency.set(c, (frequency.get(c) ?? 0) + 1);
|
|
3761
|
-
let mode = 1;
|
|
3762
|
-
let modeFreq = 0;
|
|
3763
|
-
for (const [c, f] of frequency) if (f > modeFreq || f === modeFreq && c > mode) {
|
|
3764
|
-
mode = c;
|
|
3765
|
-
modeFreq = f;
|
|
3766
|
-
}
|
|
3767
|
-
const score = mode > 1 ? modeFreq / lines.length : 0;
|
|
3768
|
-
if (score > bestScore) {
|
|
3769
|
-
best = candidate;
|
|
3770
|
-
bestScore = score;
|
|
3771
|
-
}
|
|
3772
|
-
}
|
|
3773
|
-
return best;
|
|
3774
|
-
}
|
|
3775
|
-
/** Split a single sample name on `delimiter`. Empty input returns `['']` so callers
|
|
3776
|
-
* always receive at least one token. */
|
|
3777
|
-
function splitMulti(name, delimiter) {
|
|
3778
|
-
return name.split(delimiter);
|
|
3779
|
-
}
|
|
3780
|
-
//#endregion
|
|
3781
3746
|
//#region src/composables/autoGroup/classKey.ts
|
|
3782
3747
|
function classKey(c) {
|
|
3783
3748
|
const base = c.subKind ? `${c.kind}:${c.subKind}` : c.kind;
|
|
@@ -3792,22 +3757,20 @@ var vocab_default = {
|
|
|
3792
3757
|
"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).",
|
|
3793
3758
|
"ionizationModes": "Lowercase polarity tokens (pos / positive / neg / negative). Suffixed to the subKind so POS and NEG acquisitions get independent schemas.",
|
|
3794
3759
|
"tissueParentTokens": "Tokens that flag the immediately-following token as the organ subKind (so `tissues / kidney` becomes Biological/Tissues, tagging both positions).",
|
|
3795
|
-
"
|
|
3760
|
+
"replicateToken": "Regex source for a token that names a replicate: rep1, Rep2, replicate3, R1, T1 (technical replicate). A column whose values all match is a replicate column: it stays a visible field and is left out of the default group key. B<n> is not in the pattern: on the lab share B13 / S13 are label tokens."
|
|
3796
3761
|
},
|
|
3797
3762
|
patterns: {
|
|
3798
3763
|
"blank": "^(blank|blk|solv?blank|matrix.?blank)$",
|
|
3799
|
-
"iqc": "^iqc$",
|
|
3764
|
+
"iqc": "^(iqc|qc|pqc|pooled.?qc)$",
|
|
3800
3765
|
"eqc": "^eqc$",
|
|
3801
|
-
"standard": "^(
|
|
3766
|
+
"standard": "^(standards?|stds?\\d*|istd|cal(?:ibrator)?\\d*|.+standard)$",
|
|
3802
3767
|
"spikeIn": "spike.?in",
|
|
3803
3768
|
"stock": "^stock$"
|
|
3804
3769
|
},
|
|
3805
3770
|
standardAllow: [
|
|
3806
3771
|
"ExtractionS",
|
|
3807
3772
|
"IS",
|
|
3808
|
-
"internal.standard"
|
|
3809
|
-
"PQC",
|
|
3810
|
-
"pooledQC"
|
|
3773
|
+
"internal.standard"
|
|
3811
3774
|
],
|
|
3812
3775
|
matrixVocab: {
|
|
3813
3776
|
"plasma": "Plasma",
|
|
@@ -3837,11 +3800,7 @@ var vocab_default = {
|
|
|
3837
3800
|
"negative": "NEG"
|
|
3838
3801
|
},
|
|
3839
3802
|
tissueParentTokens: ["tissue", "tissues"],
|
|
3840
|
-
|
|
3841
|
-
"[_-]T\\d+(?=[_-]|$)",
|
|
3842
|
-
"[_-]B\\d+(?=[_-]|$)",
|
|
3843
|
-
"[_-](?:rep(?:licate)?)\\d+(?=[_-]|$)"
|
|
3844
|
-
]
|
|
3803
|
+
replicateToken: "^(?:rep(?:licate)?|r|t)\\d+$"
|
|
3845
3804
|
};
|
|
3846
3805
|
//#endregion
|
|
3847
3806
|
//#region src/composables/autoGroup/classify.ts
|
|
@@ -3866,6 +3825,14 @@ function matrixCanonical(token, vocab) {
|
|
|
3866
3825
|
function isTissueParentToken(token, vocab) {
|
|
3867
3826
|
return vocab.tissueParentTokens.has(token.toLowerCase());
|
|
3868
3827
|
}
|
|
3828
|
+
/** A token that names a QC-type class (blank, QC, standard, spike-in, stock). */
|
|
3829
|
+
function isClassMarker(token, vocab = classVocab) {
|
|
3830
|
+
return vocab.blankPattern.test(token) || vocab.iqcPattern.test(token) || vocab.eqcPattern.test(token) || vocab.standardPattern.test(token) || vocab.standardAllow.has(token) || vocab.spikeInPattern.test(token) || vocab.stockPattern.test(token);
|
|
3831
|
+
}
|
|
3832
|
+
/** Kinds that form experimental groups; all other kinds are QC-type overlay classes. */
|
|
3833
|
+
function isExperimentalKind(kind) {
|
|
3834
|
+
return kind === "biological" || kind === "unknown";
|
|
3835
|
+
}
|
|
3869
3836
|
var DEFAULT_DISPOSITION = {
|
|
3870
3837
|
biological: "group",
|
|
3871
3838
|
iqc: "overlay",
|
|
@@ -3876,28 +3843,28 @@ var DEFAULT_DISPOSITION = {
|
|
|
3876
3843
|
blank: "overlay",
|
|
3877
3844
|
unknown: "group"
|
|
3878
3845
|
};
|
|
3879
|
-
function classifyTokens(tokens, vocab) {
|
|
3880
|
-
for (let i =
|
|
3846
|
+
function classifyTokens(tokens, vocab, qcFrom) {
|
|
3847
|
+
for (let i = qcFrom; i < tokens.length; i++) if (vocab.blankPattern.test(tokens[i])) return {
|
|
3881
3848
|
kind: "blank",
|
|
3882
3849
|
tagPositions: [i]
|
|
3883
3850
|
};
|
|
3884
|
-
for (let i =
|
|
3851
|
+
for (let i = qcFrom; i < tokens.length; i++) if (vocab.iqcPattern.test(tokens[i])) return {
|
|
3885
3852
|
kind: "iqc",
|
|
3886
3853
|
tagPositions: [i]
|
|
3887
3854
|
};
|
|
3888
|
-
for (let i =
|
|
3855
|
+
for (let i = qcFrom; i < tokens.length; i++) if (vocab.eqcPattern.test(tokens[i])) return {
|
|
3889
3856
|
kind: "eqc",
|
|
3890
3857
|
tagPositions: [i]
|
|
3891
3858
|
};
|
|
3892
|
-
for (let i =
|
|
3859
|
+
for (let i = qcFrom; i < tokens.length; i++) if (vocab.standardPattern.test(tokens[i]) || vocab.standardAllow.has(tokens[i])) return {
|
|
3893
3860
|
kind: "standard",
|
|
3894
3861
|
tagPositions: [i]
|
|
3895
3862
|
};
|
|
3896
|
-
for (let i =
|
|
3863
|
+
for (let i = qcFrom; i < tokens.length; i++) if (vocab.spikeInPattern.test(tokens[i])) return {
|
|
3897
3864
|
kind: "spike-in",
|
|
3898
3865
|
tagPositions: [i]
|
|
3899
3866
|
};
|
|
3900
|
-
for (let i =
|
|
3867
|
+
for (let i = qcFrom; i < tokens.length; i++) if (vocab.stockPattern.test(tokens[i])) return {
|
|
3901
3868
|
kind: "stock",
|
|
3902
3869
|
tagPositions: [i]
|
|
3903
3870
|
};
|
|
@@ -3982,7 +3949,7 @@ function detectClass(tokenizedSamples, options) {
|
|
|
3982
3949
|
kind = "blank";
|
|
3983
3950
|
tagPositions = [];
|
|
3984
3951
|
} else {
|
|
3985
|
-
const r = classifyTokens(tokenizedSamples[i], vocab);
|
|
3952
|
+
const r = classifyTokens(tokenizedSamples[i], vocab, options.sharedPrefix ?? 0);
|
|
3986
3953
|
kind = r.kind;
|
|
3987
3954
|
subKind = r.subKind;
|
|
3988
3955
|
tagPositions = r.tagPositions;
|
|
@@ -4012,11 +3979,174 @@ function detectClass(tokenizedSamples, options) {
|
|
|
4012
3979
|
return [...buckets.values()];
|
|
4013
3980
|
}
|
|
4014
3981
|
//#endregion
|
|
3982
|
+
//#region src/composables/autoGroup/tokenize.ts
|
|
3983
|
+
var DELIMITER_CANDIDATES = [
|
|
3984
|
+
"_",
|
|
3985
|
+
"-",
|
|
3986
|
+
" ",
|
|
3987
|
+
"."
|
|
3988
|
+
];
|
|
3989
|
+
/** Pick the delimiter whose split produces the most consistent field count across `lines`. */
|
|
3990
|
+
function pickPrimaryDelimiter(lines) {
|
|
3991
|
+
if (lines.length === 0) return "_";
|
|
3992
|
+
let best = "_";
|
|
3993
|
+
let bestScore = -1;
|
|
3994
|
+
for (const candidate of DELIMITER_CANDIDATES) {
|
|
3995
|
+
const counts = lines.map((l) => l.split(candidate).length);
|
|
3996
|
+
const frequency = /* @__PURE__ */ new Map();
|
|
3997
|
+
for (const c of counts) frequency.set(c, (frequency.get(c) ?? 0) + 1);
|
|
3998
|
+
let mode = 1;
|
|
3999
|
+
let modeFreq = 0;
|
|
4000
|
+
for (const [c, f] of frequency) if (f > modeFreq || f === modeFreq && c > mode) {
|
|
4001
|
+
mode = c;
|
|
4002
|
+
modeFreq = f;
|
|
4003
|
+
}
|
|
4004
|
+
const score = mode > 1 ? modeFreq / lines.length : 0;
|
|
4005
|
+
if (score > bestScore) {
|
|
4006
|
+
best = candidate;
|
|
4007
|
+
bestScore = score;
|
|
4008
|
+
}
|
|
4009
|
+
}
|
|
4010
|
+
return best;
|
|
4011
|
+
}
|
|
4012
|
+
var HEADER_FIELDS = [
|
|
4013
|
+
"code",
|
|
4014
|
+
"date",
|
|
4015
|
+
"operator"
|
|
4016
|
+
];
|
|
4017
|
+
var OPERATOR_AGREEMENT = .9;
|
|
4018
|
+
var FIRST_PLAUSIBLE_YEAR = 2015;
|
|
4019
|
+
function isoDate(year, month, day) {
|
|
4020
|
+
const lastYear = (/* @__PURE__ */ new Date()).getFullYear() + 1;
|
|
4021
|
+
if (year < FIRST_PLAUSIBLE_YEAR || year > lastYear) return void 0;
|
|
4022
|
+
const date = new Date(Date.UTC(year, month - 1, day));
|
|
4023
|
+
if (date.getUTCMonth() !== month - 1 || date.getUTCDate() !== day) return void 0;
|
|
4024
|
+
return date.toISOString().slice(0, 10);
|
|
4025
|
+
}
|
|
4026
|
+
/** Read a 6- or 8-digit date token; year-first is tried before day-first. */
|
|
4027
|
+
function parseDateToken(token) {
|
|
4028
|
+
const n = (from, to) => Number(token.slice(from, to));
|
|
4029
|
+
if (/^\d{8}$/.test(token)) return isoDate(n(0, 4), n(4, 6), n(6, 8)) ?? isoDate(n(4, 8), n(2, 4), n(0, 2));
|
|
4030
|
+
if (/^\d{6}$/.test(token)) return isoDate(2e3 + n(0, 2), n(2, 4), n(4, 6)) ?? isoDate(2e3 + n(4, 6), n(2, 4), n(0, 2));
|
|
4031
|
+
}
|
|
4032
|
+
/**
|
|
4033
|
+
* Read the experiment code, date and operator from the left of a name, in any
|
|
4034
|
+
* order. `position` holds the token index of each field. A name with no code
|
|
4035
|
+
* and no date has no header, so `WT_Ctrl_1` keeps `WT` as a sample token.
|
|
4036
|
+
*/
|
|
4037
|
+
function readHeader(tokens) {
|
|
4038
|
+
const fields = {};
|
|
4039
|
+
const position = {};
|
|
4040
|
+
let i = 0;
|
|
4041
|
+
if (tokens.length > 1 && /^exp\d+$/i.test(tokens[0])) {
|
|
4042
|
+
fields.code = tokens[0].toLowerCase();
|
|
4043
|
+
position.code = i++;
|
|
4044
|
+
}
|
|
4045
|
+
for (; i < tokens.length - 1; i++) {
|
|
4046
|
+
const date = fields.date ? void 0 : parseDateToken(tokens[i]);
|
|
4047
|
+
if (date) {
|
|
4048
|
+
fields.date = date;
|
|
4049
|
+
position.date = i;
|
|
4050
|
+
} else if (!fields.operator && /^[A-Z]{2,3}$/.test(tokens[i]) && !isClassMarker(tokens[i])) {
|
|
4051
|
+
fields.operator = tokens[i];
|
|
4052
|
+
position.operator = i;
|
|
4053
|
+
} else break;
|
|
4054
|
+
}
|
|
4055
|
+
if (!fields.code && !fields.date) return {
|
|
4056
|
+
fields: {},
|
|
4057
|
+
position: {}
|
|
4058
|
+
};
|
|
4059
|
+
return {
|
|
4060
|
+
fields,
|
|
4061
|
+
position
|
|
4062
|
+
};
|
|
4063
|
+
}
|
|
4064
|
+
var MS_FILE_EXTENSION = /\.(raw|mzml|mzxml|mzdata|wiff2?|d|mgf|csv|tsv|txt)$/i;
|
|
4065
|
+
/** Drop a mass-spec file extension from each name, and any other extension that all names share. */
|
|
4066
|
+
function stripExtensions(names) {
|
|
4067
|
+
const bare = names.map((name) => name.replace(MS_FILE_EXTENSION, ""));
|
|
4068
|
+
const ext = /\.[A-Za-z][A-Za-z0-9]{0,4}$/.exec(bare[0] ?? "")?.[0];
|
|
4069
|
+
return ext && bare.every((name) => name.endsWith(ext)) ? bare.map((name) => name.slice(0, -ext.length)) : bare;
|
|
4070
|
+
}
|
|
4071
|
+
/**
|
|
4072
|
+
* Sample names → token rows.
|
|
4073
|
+
*
|
|
4074
|
+
* 1. Trim; drop a file extension that all names share.
|
|
4075
|
+
* 2. Split on runs of the primary delimiter.
|
|
4076
|
+
* 3. Drop a trailing 14-digit acquisition stamp.
|
|
4077
|
+
* 4. Read the experiment header. A field moves to `experiment` when the batch
|
|
4078
|
+
* has one value for it. Any other field stays in the rows as written, so
|
|
4079
|
+
* `QC` or `WT` after the date in some names is not taken for operator
|
|
4080
|
+
* initials.
|
|
4081
|
+
* 5. Count the tokens that all names share after the header (the experiment
|
|
4082
|
+
* title), so a title word such as `std` cannot classify the batch as QC.
|
|
4083
|
+
*/
|
|
4084
|
+
function tokenizeBatch(names) {
|
|
4085
|
+
const cleaned = stripExtensions(names.map((name) => name.trim()));
|
|
4086
|
+
const delimiter = pickPrimaryDelimiter(cleaned);
|
|
4087
|
+
const parsed = cleaned.map((name) => {
|
|
4088
|
+
const split = name.split(delimiter).filter(Boolean);
|
|
4089
|
+
if (split.length > 1 && /^\d{14}$/.test(split[split.length - 1])) split.pop();
|
|
4090
|
+
const tokens = split.length > 0 ? split : [name];
|
|
4091
|
+
return {
|
|
4092
|
+
tokens,
|
|
4093
|
+
...readHeader(tokens)
|
|
4094
|
+
};
|
|
4095
|
+
});
|
|
4096
|
+
const experiment = {};
|
|
4097
|
+
for (const field of ["code", "date"]) {
|
|
4098
|
+
const values = new Set(parsed.map(({ fields }) => fields[field]).filter(Boolean));
|
|
4099
|
+
if (values.size === 1) experiment[field] = [...values][0];
|
|
4100
|
+
}
|
|
4101
|
+
const dated = parsed.filter(({ fields }) => fields.date);
|
|
4102
|
+
const complete = dated.length > 0 ? dated : parsed.filter(({ fields }) => fields.code);
|
|
4103
|
+
const operatorCounts = /* @__PURE__ */ new Map();
|
|
4104
|
+
for (const { fields } of complete) if (fields.operator) operatorCounts.set(fields.operator, (operatorCounts.get(fields.operator) ?? 0) + 1);
|
|
4105
|
+
const [operator, count] = [...operatorCounts].sort((a, b) => b[1] - a[1])[0] ?? [];
|
|
4106
|
+
if (operator && count >= complete.length * OPERATOR_AGREEMENT) experiment.operator = operator;
|
|
4107
|
+
const rows = parsed.map(({ tokens, fields, position }) => {
|
|
4108
|
+
const moved = new Set(HEADER_FIELDS.filter((field) => experiment[field] && fields[field] === experiment[field]).map((field) => position[field]));
|
|
4109
|
+
return tokens.filter((_, index) => !moved.has(index));
|
|
4110
|
+
});
|
|
4111
|
+
let sharedPrefix = 0;
|
|
4112
|
+
if (rows.length > 1 && Object.keys(experiment).length > 0) {
|
|
4113
|
+
while (rows.every((row) => row.length > sharedPrefix + 1 && row[sharedPrefix] === rows[0][sharedPrefix])) sharedPrefix++;
|
|
4114
|
+
if (rows.every((row) => row.slice(sharedPrefix).every((token) => /^\d+[A-Za-z]?$/.test(token)))) sharedPrefix = 0;
|
|
4115
|
+
}
|
|
4116
|
+
return {
|
|
4117
|
+
delimiter,
|
|
4118
|
+
experiment,
|
|
4119
|
+
rows,
|
|
4120
|
+
sharedPrefix
|
|
4121
|
+
};
|
|
4122
|
+
}
|
|
4123
|
+
//#endregion
|
|
4015
4124
|
//#region src/composables/autoGroup/columns.ts
|
|
4016
|
-
var NUMERIC_VALUE_UNIT = /^(\d+(?:dot\d+)?)([A-Za-z%/μ]+)$/;
|
|
4125
|
+
var NUMERIC_VALUE_UNIT = /^(\d+(?:(?:dot|\.)\d+)?)([A-Za-z%/μ]+)$/;
|
|
4017
4126
|
var RATIO = /^(\d+)to(\d+)$/;
|
|
4018
4127
|
var RUN_ORDER_PATTERN = /^\d{2,4}[A-Z]?$/;
|
|
4019
|
-
var REPLICATE_TOKEN_PATTERN =
|
|
4128
|
+
var REPLICATE_TOKEN_PATTERN = new RegExp(vocab_default.replicateToken, "i");
|
|
4129
|
+
/**
|
|
4130
|
+
* A replicate counter is a dense run of small numbers or letters: `1, 2, 3`,
|
|
4131
|
+
* `01, 02, 03` or `A, B, C`. A quarter of the run can be missing (a failed
|
|
4132
|
+
* sample). `1, 5, 10` is a dose and `1, 2, 6, 7` an animal number: too sparse.
|
|
4133
|
+
*/
|
|
4134
|
+
function isCounterRun(values) {
|
|
4135
|
+
if (!values.every((value) => /^(\d{1,2}|[A-Za-z])$/.test(value))) return false;
|
|
4136
|
+
const ranks = [...new Set(values.map((value) => /\d/.test(value) ? Number(value) : value.toUpperCase().charCodeAt(0) - 64))];
|
|
4137
|
+
const allowedGaps = Math.max(1, Math.floor(ranks.length / 4));
|
|
4138
|
+
return Math.min(...ranks) >= 1 && Math.max(...ranks) <= ranks.length + allowedGaps;
|
|
4139
|
+
}
|
|
4140
|
+
var STEM_COUNTER_PATTERN = /^([A-Za-z]+)(\d{1,2})$/;
|
|
4141
|
+
var COLUMN_AGREEMENT = .9;
|
|
4142
|
+
var ID_MIN_CLASS_SIZE = 4;
|
|
4143
|
+
/** Roles the Field Recipe offers as grouping fields. `replicate` and `id` are off by default. */
|
|
4144
|
+
var GROUPING_FIELD_ROLES = /* @__PURE__ */ new Set([
|
|
4145
|
+
"factor",
|
|
4146
|
+
"numeric",
|
|
4147
|
+
"replicate",
|
|
4148
|
+
"id"
|
|
4149
|
+
]);
|
|
4020
4150
|
var DEFAULT_FACTOR_CARDINALITY_MAX = 50;
|
|
4021
4151
|
var LOW_CARDINALITY_NUMERIC_AS_FACTOR_MAX = 5;
|
|
4022
4152
|
function parseNumeric(token) {
|
|
@@ -4044,8 +4174,12 @@ function parseNumeric(token) {
|
|
|
4044
4174
|
}
|
|
4045
4175
|
function inferRole(input) {
|
|
4046
4176
|
const { values, position, totalPositions, classSize } = input;
|
|
4177
|
+
const tail = input.tail ?? position === totalPositions - 1;
|
|
4047
4178
|
const cardinality = new Set(values).size;
|
|
4048
4179
|
if (cardinality === 1) return "constant";
|
|
4180
|
+
const present = values.filter(Boolean);
|
|
4181
|
+
if (present.every((v) => REPLICATE_TOKEN_PATTERN.test(v))) return "replicate";
|
|
4182
|
+
if (tail && cardinality * 2 <= values.length && isCounterRun(present)) return "replicate";
|
|
4049
4183
|
if (position === totalPositions - 1) {
|
|
4050
4184
|
if (values.every((v) => RUN_ORDER_PATTERN.test(v)) && cardinality >= classSize * .5) return "run-order";
|
|
4051
4185
|
}
|
|
@@ -4053,13 +4187,56 @@ function inferRole(input) {
|
|
|
4053
4187
|
if (cardinality <= LOW_CARDINALITY_NUMERIC_AS_FACTOR_MAX) return "factor";
|
|
4054
4188
|
return "numeric";
|
|
4055
4189
|
}
|
|
4056
|
-
if (values.
|
|
4190
|
+
if (values.length >= ID_MIN_CLASS_SIZE && cardinality >= values.length * COLUMN_AGREEMENT && values.filter((v) => /\d/.test(v)).length >= values.length * COLUMN_AGREEMENT) return "id";
|
|
4057
4191
|
if (cardinality <= DEFAULT_FACTOR_CARDINALITY_MAX) return "factor";
|
|
4058
4192
|
return "ignore";
|
|
4059
4193
|
}
|
|
4060
4194
|
/**
|
|
4195
|
+
* How a column splits when most of its values agree: on `-` into equal parts
|
|
4196
|
+
* (`2-1` → `2`, `1`), or into stem + counter (`C1` → `C`, `1`) when two or more
|
|
4197
|
+
* stems repeat with different counters. `D1`/`D3` (one stem) and `B13`/`S13`
|
|
4198
|
+
* (one counter for each stem) stay whole.
|
|
4199
|
+
*/
|
|
4200
|
+
function columnSplitter(values) {
|
|
4201
|
+
const agree = (test) => values.filter(test).length >= values.length * COLUMN_AGREEMENT;
|
|
4202
|
+
const partCounts = /* @__PURE__ */ new Map();
|
|
4203
|
+
for (const value of values) {
|
|
4204
|
+
const parts = value.split("-").length;
|
|
4205
|
+
partCounts.set(parts, (partCounts.get(parts) ?? 0) + 1);
|
|
4206
|
+
}
|
|
4207
|
+
const parts = [...partCounts].sort((a, b) => b[1] - a[1])[0]?.[0] ?? 1;
|
|
4208
|
+
if (parts > 1 && agree((value) => value.split("-").length === parts)) return (value) => {
|
|
4209
|
+
const pieces = value.split("-");
|
|
4210
|
+
return pieces.length === parts ? pieces : [value, ...Array(parts - 1).fill("")];
|
|
4211
|
+
};
|
|
4212
|
+
const countersByStem = /* @__PURE__ */ new Map();
|
|
4213
|
+
for (const value of values) {
|
|
4214
|
+
const match = STEM_COUNTER_PATTERN.exec(value);
|
|
4215
|
+
if (match) countersByStem.set(match[1], (countersByStem.get(match[1]) ?? /* @__PURE__ */ new Set()).add(match[2]));
|
|
4216
|
+
}
|
|
4217
|
+
const repeated = [...countersByStem.values()].filter((counters) => counters.size > 1).length;
|
|
4218
|
+
if (countersByStem.size > 1 && repeated * 2 >= countersByStem.size && agree((value) => STEM_COUNTER_PATTERN.test(value))) return (value) => STEM_COUNTER_PATTERN.exec(value)?.slice(1) ?? [value, ""];
|
|
4219
|
+
}
|
|
4220
|
+
/** Split the columns of one class (rows of equal length) that hold two fields in one token. */
|
|
4221
|
+
function splitColumns(rows) {
|
|
4222
|
+
const width = rows[0]?.length ?? 0;
|
|
4223
|
+
const splitters = Array.from({ length: width }, (_, col) => columnSplitter(rows.map((row) => row[col] ?? "")));
|
|
4224
|
+
const origin = [];
|
|
4225
|
+
const tail = [];
|
|
4226
|
+
splitters.forEach((splitter, col) => {
|
|
4227
|
+
const parts = splitter ? splitter(rows[0][col]).length : 1;
|
|
4228
|
+
for (let part = 0; part < parts; part++) origin.push(col);
|
|
4229
|
+
if (parts > 1) tail.push(origin.length - 1);
|
|
4230
|
+
});
|
|
4231
|
+
return {
|
|
4232
|
+
rows: rows.map((row) => row.flatMap((token, col) => splitters[col]?.(token) ?? [token])),
|
|
4233
|
+
origin,
|
|
4234
|
+
tail
|
|
4235
|
+
};
|
|
4236
|
+
}
|
|
4237
|
+
/**
|
|
4061
4238
|
* Auto-detect a friendlier column name from the column's values + role.
|
|
4062
|
-
* Returns null when nothing matches, so callers can fall back to "
|
|
4239
|
+
* Returns null when nothing matches, so callers can fall back to "Field N".
|
|
4063
4240
|
*
|
|
4064
4241
|
* Heuristics are content-derived and deterministic. They cover the common
|
|
4065
4242
|
* LC-MS / proteomics naming conventions; users can override anything via
|
|
@@ -4134,15 +4311,18 @@ function inferColumnName(values, uniqueValues, role) {
|
|
|
4134
4311
|
if (values.every((v) => /^(pos|neg|positive|negative)$/i.test(v))) return "Polarity";
|
|
4135
4312
|
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";
|
|
4136
4313
|
if (values.every((v) => /^(exp|expt|study)\d+$/i.test(v))) return "Experiment";
|
|
4137
|
-
if (uniqueValues.length
|
|
4314
|
+
if (uniqueValues.length === 1 && /^[A-Z]{2,3}$/.test(uniqueValues[0])) return "Username";
|
|
4315
|
+
if (role === "replicate") return "Replicate";
|
|
4316
|
+
if (role === "id") return "Sample ID";
|
|
4138
4317
|
return null;
|
|
4139
4318
|
}
|
|
4140
|
-
function buildClassSchema(tokenizedMembers, classKind, subKind, classTagPositions) {
|
|
4319
|
+
function buildClassSchema(tokenizedMembers, classKind, subKind, classTagPositions, tailPositions = []) {
|
|
4141
4320
|
const totalPositions = tokenizedMembers[0]?.length ?? 0;
|
|
4142
4321
|
const classSize = tokenizedMembers.length;
|
|
4143
4322
|
const columns = [];
|
|
4144
4323
|
const tagSet = new Set(classTagPositions);
|
|
4145
|
-
|
|
4324
|
+
const tailSet = /* @__PURE__ */ new Set([...tailPositions, totalPositions - 1]);
|
|
4325
|
+
for (let pos = totalPositions - 1; pos >= 0; pos--) {
|
|
4146
4326
|
const values = tokenizedMembers.map((t) => t[pos] ?? "");
|
|
4147
4327
|
const uniqueValues = [...new Set(values)];
|
|
4148
4328
|
let role;
|
|
@@ -4151,13 +4331,16 @@ function buildClassSchema(tokenizedMembers, classKind, subKind, classTagPosition
|
|
|
4151
4331
|
values,
|
|
4152
4332
|
position: pos,
|
|
4153
4333
|
totalPositions,
|
|
4154
|
-
classSize
|
|
4334
|
+
classSize,
|
|
4335
|
+
tail: tailSet.has(pos)
|
|
4155
4336
|
});
|
|
4337
|
+
const lastPosition = pos === totalPositions - 1;
|
|
4338
|
+
if (role === "run-order" || lastPosition && role === "id") tailSet.add(pos - 1);
|
|
4156
4339
|
const numeric = role === "numeric" ? { byValue: Object.fromEntries(uniqueValues.map((v) => [v, parseNumeric(v)]).filter((e) => e[1] !== null)) } : void 0;
|
|
4157
4340
|
const inferredName = inferColumnName(values, uniqueValues, role);
|
|
4158
|
-
columns.
|
|
4341
|
+
columns.unshift({
|
|
4159
4342
|
index: pos,
|
|
4160
|
-
name: inferredName ?? `
|
|
4343
|
+
name: inferredName ?? `Field ${pos + 1}`,
|
|
4161
4344
|
sourceIndices: [pos],
|
|
4162
4345
|
uniqueValues,
|
|
4163
4346
|
cardinality: uniqueValues.length,
|
|
@@ -4166,16 +4349,18 @@ function buildClassSchema(tokenizedMembers, classKind, subKind, classTagPosition
|
|
|
4166
4349
|
type: pos === 0 ? "prefix" : "suffix"
|
|
4167
4350
|
});
|
|
4168
4351
|
}
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4352
|
+
const nameCounts = /* @__PURE__ */ new Map();
|
|
4353
|
+
const isField = (c) => GROUPING_FIELD_ROLES.has(c.role);
|
|
4354
|
+
for (const c of [...columns.filter(isField), ...columns.filter((c) => !isField(c))]) {
|
|
4355
|
+
const count = (nameCounts.get(c.name) ?? 0) + 1;
|
|
4356
|
+
nameCounts.set(c.name, count);
|
|
4357
|
+
if (count > 1) c.name = `${c.name} ${count}`;
|
|
4173
4358
|
}
|
|
4174
4359
|
return {
|
|
4175
4360
|
classKind,
|
|
4176
4361
|
subKind,
|
|
4177
4362
|
columns,
|
|
4178
|
-
groupBy: columns.filter((c) => c.role === "factor").map((c) => c.index)
|
|
4363
|
+
groupBy: columns.filter((c) => c.role === "factor" || c.role === "numeric").map((c) => c.index)
|
|
4179
4364
|
};
|
|
4180
4365
|
}
|
|
4181
4366
|
//#endregion
|
|
@@ -4315,8 +4500,8 @@ function composeGroups(input) {
|
|
|
4315
4500
|
return fieldValues[col.displayName ?? col.name];
|
|
4316
4501
|
}) : [cls.label];
|
|
4317
4502
|
const hasMissingGroupPart = input.missingGroupName && cls.disposition === "group" && keyParts.some((part) => part.trim().length === 0);
|
|
4318
|
-
const prefix = cls.kind !== "unknown"
|
|
4319
|
-
const groupKey = hasMissingGroupPart ? input.missingGroupName : prefix ?
|
|
4503
|
+
const prefix = cls.kind !== "unknown" ? classPrefix.get(cls) : void 0;
|
|
4504
|
+
const groupKey = hasMissingGroupPart ? input.missingGroupName : [...prefix ? [prefix] : [], ...keyParts].join(" / ") || cls.label;
|
|
4320
4505
|
const bucket = groupMap.get(groupKey);
|
|
4321
4506
|
if (bucket) bucket.samples.push(rowName);
|
|
4322
4507
|
else groupMap.set(groupKey, {
|
|
@@ -4414,7 +4599,7 @@ function serializeFingerprint(schemas) {
|
|
|
4414
4599
|
}
|
|
4415
4600
|
function restoreFingerprint(fp, current) {
|
|
4416
4601
|
return fp.classes.map((snap) => {
|
|
4417
|
-
const candidates = current.filter((c) => c.classKind === snap.kind && c.subKind === snap.subKind && (snap.tokenLength == null || c.tokenLength === snap.tokenLength));
|
|
4602
|
+
const candidates = current.filter((c) => c.classKind === snap.kind && c.subKind === snap.subKind && (snap.tokenLength == null || c.tokenLength == null || c.tokenLength === snap.tokenLength));
|
|
4418
4603
|
const target = candidates.find((c) => c.columns.length === snap.columns.length) ?? candidates[0];
|
|
4419
4604
|
if (!target) throw new Error(`Fingerprint class not present in current input: ${snap.kind}${snap.subKind ? "/" + snap.subKind : ""}`);
|
|
4420
4605
|
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`);
|
|
@@ -4538,91 +4723,6 @@ function composeTemplate(samples, schemas, classes, options) {
|
|
|
4538
4723
|
};
|
|
4539
4724
|
}
|
|
4540
4725
|
//#endregion
|
|
4541
|
-
//#region src/composables/autoGroup/replicatePreGroup.ts
|
|
4542
|
-
var STRIP_PATTERNS = vocab_default.replicateStripPatterns.map((src) => new RegExp(src, "gi"));
|
|
4543
|
-
/**
|
|
4544
|
-
* Iteratively strip every replicate / injection-number marker from `name`.
|
|
4545
|
-
* Each pattern is applied repeatedly until it stops matching, so a sample like
|
|
4546
|
-
* `study_T1_Rep2_004` collapses to `study` in one call.
|
|
4547
|
-
*/
|
|
4548
|
-
function stripReplicateTokens(name) {
|
|
4549
|
-
let base = name;
|
|
4550
|
-
for (const re of STRIP_PATTERNS) {
|
|
4551
|
-
let prev;
|
|
4552
|
-
do {
|
|
4553
|
-
prev = base;
|
|
4554
|
-
base = base.replace(re, "");
|
|
4555
|
-
re.lastIndex = 0;
|
|
4556
|
-
} while (base !== prev);
|
|
4557
|
-
}
|
|
4558
|
-
return base;
|
|
4559
|
-
}
|
|
4560
|
-
function preGroupReplicates(samples) {
|
|
4561
|
-
const map = /* @__PURE__ */ new Map();
|
|
4562
|
-
const baseOrder = [];
|
|
4563
|
-
for (let i = 0; i < samples.length; i++) {
|
|
4564
|
-
const base = stripReplicateTokens(samples[i]);
|
|
4565
|
-
const bucket = map.get(base);
|
|
4566
|
-
if (bucket) bucket.push(i);
|
|
4567
|
-
else {
|
|
4568
|
-
map.set(base, [i]);
|
|
4569
|
-
baseOrder.push(base);
|
|
4570
|
-
}
|
|
4571
|
-
}
|
|
4572
|
-
return {
|
|
4573
|
-
baseNames: baseOrder,
|
|
4574
|
-
membersByBase: baseOrder.map((b) => map.get(b)),
|
|
4575
|
-
originalSamples: samples
|
|
4576
|
-
};
|
|
4577
|
-
}
|
|
4578
|
-
/**
|
|
4579
|
-
* Expand groups produced over base names back to groups of original sample
|
|
4580
|
-
* names. Each entry in `group.samples` is treated as a base name and replaced
|
|
4581
|
-
* by the list of original samples that collapsed onto it.
|
|
4582
|
-
*/
|
|
4583
|
-
function expandGroupsWithReplicates(groups, preGrouping) {
|
|
4584
|
-
const baseToMembers = /* @__PURE__ */ new Map();
|
|
4585
|
-
for (let i = 0; i < preGrouping.baseNames.length; i++) baseToMembers.set(preGrouping.baseNames[i], preGrouping.membersByBase[i]);
|
|
4586
|
-
return groups.map((g) => ({
|
|
4587
|
-
...g,
|
|
4588
|
-
samples: g.samples.flatMap((baseName) => {
|
|
4589
|
-
const indices = baseToMembers.get(baseName);
|
|
4590
|
-
return indices ? indices.map((i) => preGrouping.originalSamples[i]) : [baseName];
|
|
4591
|
-
})
|
|
4592
|
-
}));
|
|
4593
|
-
}
|
|
4594
|
-
/**
|
|
4595
|
-
* Expand a group hierarchy (built over base names) back to original samples.
|
|
4596
|
-
* Each leaf node's `label` is a base name; it is replaced by one leaf per
|
|
4597
|
-
* original sample that collapsed onto it (sharing the leaf's injection badge,
|
|
4598
|
-
* since collapsed replicates have identical base tokens). Every ancestor's
|
|
4599
|
-
* `badge` (sample count) is recomputed from the expanded leaf count so the tree
|
|
4600
|
-
* agrees with the flat groups produced by `expandGroupsWithReplicates`.
|
|
4601
|
-
*/
|
|
4602
|
-
function expandTreeWithReplicates(nodes, preGrouping) {
|
|
4603
|
-
const baseToOriginals = /* @__PURE__ */ new Map();
|
|
4604
|
-
for (let i = 0; i < preGrouping.baseNames.length; i++) baseToOriginals.set(preGrouping.baseNames[i], preGrouping.membersByBase[i].map((m) => preGrouping.originalSamples[m]));
|
|
4605
|
-
const leafCount = (n) => n.children && n.children.length ? n.children.reduce((acc, c) => acc + leafCount(c), 0) : 1;
|
|
4606
|
-
const expand = (node) => {
|
|
4607
|
-
if (node.children && node.children.length) {
|
|
4608
|
-
const children = node.children.flatMap(expand);
|
|
4609
|
-
return [{
|
|
4610
|
-
...node,
|
|
4611
|
-
children,
|
|
4612
|
-
badge: children.reduce((acc, c) => acc + leafCount(c), 0)
|
|
4613
|
-
}];
|
|
4614
|
-
}
|
|
4615
|
-
const originals = baseToOriginals.get(node.label);
|
|
4616
|
-
if (!originals || originals.length === 0) return [node];
|
|
4617
|
-
return originals.map((name, k) => ({
|
|
4618
|
-
...node,
|
|
4619
|
-
id: `${node.id}${k}`,
|
|
4620
|
-
label: name
|
|
4621
|
-
}));
|
|
4622
|
-
};
|
|
4623
|
-
return nodes.flatMap(expand);
|
|
4624
|
-
}
|
|
4625
|
-
//#endregion
|
|
4626
4726
|
//#region src/composables/autoGroup/tokenLength.ts
|
|
4627
4727
|
/**
|
|
4628
4728
|
* Token-length pre-grouping.
|
|
@@ -4642,6 +4742,10 @@ function expandTreeWithReplicates(nodes, preGrouping) {
|
|
|
4642
4742
|
function splitByTokenLength(classes, tokenized) {
|
|
4643
4743
|
const out = [];
|
|
4644
4744
|
for (const cls of classes) {
|
|
4745
|
+
if (!isExperimentalKind(cls.kind)) {
|
|
4746
|
+
out.push(cls);
|
|
4747
|
+
continue;
|
|
4748
|
+
}
|
|
4645
4749
|
const byLength = /* @__PURE__ */ new Map();
|
|
4646
4750
|
const lengthOrder = [];
|
|
4647
4751
|
for (const m of cls.members) {
|
|
@@ -4793,7 +4897,7 @@ function useAutoGroup() {
|
|
|
4793
4897
|
});
|
|
4794
4898
|
const tokenized = ref([]);
|
|
4795
4899
|
const delimiter = ref("_");
|
|
4796
|
-
const
|
|
4900
|
+
const experiment = ref({});
|
|
4797
4901
|
function parseInput() {
|
|
4798
4902
|
if ((inputMode.value === "csv" || inputMode.value === "experiment") && csvData.value) {
|
|
4799
4903
|
parseInputFromCsv();
|
|
@@ -4802,7 +4906,7 @@ function useAutoGroup() {
|
|
|
4802
4906
|
parseInputFromPaste();
|
|
4803
4907
|
}
|
|
4804
4908
|
function parseInputFromCsv() {
|
|
4805
|
-
|
|
4909
|
+
experiment.value = {};
|
|
4806
4910
|
const csv = csvData.value;
|
|
4807
4911
|
const nonSampleCols = csv.columns.filter((c) => c !== csv.sampleColumn && c !== "sample_type");
|
|
4808
4912
|
if (nonSampleCols.length === 0) {
|
|
@@ -4847,25 +4951,40 @@ function useAutoGroup() {
|
|
|
4847
4951
|
if (lines.length === 0) {
|
|
4848
4952
|
classes.value = [];
|
|
4849
4953
|
schemas.value = {};
|
|
4850
|
-
|
|
4954
|
+
experiment.value = {};
|
|
4851
4955
|
return;
|
|
4852
4956
|
}
|
|
4853
|
-
const
|
|
4854
|
-
|
|
4855
|
-
|
|
4856
|
-
|
|
4857
|
-
|
|
4858
|
-
|
|
4859
|
-
|
|
4860
|
-
|
|
4957
|
+
const batch = tokenizeBatch(lines);
|
|
4958
|
+
delimiter.value = batch.delimiter;
|
|
4959
|
+
experiment.value = batch.experiment;
|
|
4960
|
+
const tokens = batch.rows;
|
|
4961
|
+
const detected = splitByTokenLength(detectClass(tokens, {
|
|
4962
|
+
sampleTypeHints: sampleTypeHints.value,
|
|
4963
|
+
sharedPrefix: batch.sharedPrefix
|
|
4964
|
+
}), tokens);
|
|
4861
4965
|
const newSchemas = {};
|
|
4862
4966
|
for (const cls of detected) {
|
|
4863
|
-
|
|
4967
|
+
let memberTokens = cls.members.map((i) => tokens[i]);
|
|
4968
|
+
let tailPositions = [];
|
|
4969
|
+
const experimental = isExperimentalKind(cls.kind);
|
|
4970
|
+
if (experimental) {
|
|
4971
|
+
const split = splitColumns(memberTokens);
|
|
4972
|
+
cls.members.forEach((member, row) => {
|
|
4973
|
+
tokens[member] = split.rows[row];
|
|
4974
|
+
});
|
|
4975
|
+
cls.classTagPositions = split.origin.flatMap((source, index) => cls.classTagPositions.includes(source) ? [index] : []);
|
|
4976
|
+
memberTokens = split.rows;
|
|
4977
|
+
tailPositions = split.tail;
|
|
4978
|
+
}
|
|
4979
|
+
const schema = buildClassSchema(memberTokens, cls.kind, cls.subKind, cls.classTagPositions, tailPositions);
|
|
4980
|
+
if (!experimental) schema.groupBy = [];
|
|
4864
4981
|
newSchemas[classKey(cls)] = {
|
|
4865
|
-
...
|
|
4982
|
+
...schema,
|
|
4866
4983
|
tokenLength: cls.tokenLength
|
|
4867
4984
|
};
|
|
4868
4985
|
}
|
|
4986
|
+
tokenized.value = tokens;
|
|
4987
|
+
classes.value = detected;
|
|
4869
4988
|
schemas.value = newSchemas;
|
|
4870
4989
|
if (detected.length > 0) activeClassKey.value = classKey(detected[0]);
|
|
4871
4990
|
}
|
|
@@ -4877,25 +4996,17 @@ function useAutoGroup() {
|
|
|
4877
4996
|
if (!activeClass) return [];
|
|
4878
4997
|
return findMerges(schema, activeClass.members.map((i) => tokenized.value[i]));
|
|
4879
4998
|
});
|
|
4880
|
-
|
|
4881
|
-
|
|
4882
|
-
const composed = composeGroups({
|
|
4999
|
+
function composeWith(withSchemas) {
|
|
5000
|
+
return composeGroups({
|
|
4883
5001
|
tokenizedSamples: tokenized.value,
|
|
4884
|
-
sampleNames:
|
|
4885
|
-
schemas:
|
|
5002
|
+
sampleNames: samples.value,
|
|
5003
|
+
schemas: withSchemas,
|
|
4886
5004
|
classes: classes.value,
|
|
4887
5005
|
prefixClassLabels: !(csvData.value && (inputMode.value === "csv" || inputMode.value === "experiment")),
|
|
4888
5006
|
missingGroupName: csvData.value && (inputMode.value === "csv" || inputMode.value === "experiment") ? STRUCTURED_MISSING_GROUP : void 0
|
|
4889
5007
|
});
|
|
4890
|
-
|
|
4891
|
-
|
|
4892
|
-
...composed,
|
|
4893
|
-
groups: expandGroupsWithReplicates(composed.groups, pre),
|
|
4894
|
-
experimentalGroups: composed.experimentalGroups ? expandGroupsWithReplicates(composed.experimentalGroups, pre) : void 0,
|
|
4895
|
-
qcGroups: composed.qcGroups ? expandGroupsWithReplicates(composed.qcGroups, pre) : void 0,
|
|
4896
|
-
groupTree: composed.groupTree ? expandTreeWithReplicates(composed.groupTree, pre) : composed.groupTree
|
|
4897
|
-
};
|
|
4898
|
-
});
|
|
5008
|
+
}
|
|
5009
|
+
const result = computed(() => composeWith(schemas.value));
|
|
4899
5010
|
const fingerprint = computed(() => serializeFingerprint(result.value.schemas ?? []));
|
|
4900
5011
|
const groups = computed(() => result.value.groups);
|
|
4901
5012
|
const qcGroups = computed(() => result.value.qcGroups ?? []);
|
|
@@ -4955,7 +5066,8 @@ function useAutoGroup() {
|
|
|
4955
5066
|
}
|
|
4956
5067
|
function schemasExposing(fieldKey) {
|
|
4957
5068
|
return Object.entries(schemas.value).flatMap(([key, schema]) => {
|
|
4958
|
-
|
|
5069
|
+
if (!isExperimentalKind(schema.classKind)) return [];
|
|
5070
|
+
const col = schema.columns.find((c) => (c.displayName ?? c.name) === fieldKey && (GROUPING_FIELD_ROLES.has(c.role) || schema.groupBy.includes(c.index)));
|
|
4959
5071
|
return col ? [{
|
|
4960
5072
|
key,
|
|
4961
5073
|
index: col.index
|
|
@@ -4982,6 +5094,26 @@ function useAutoGroup() {
|
|
|
4982
5094
|
}
|
|
4983
5095
|
}
|
|
4984
5096
|
/**
|
|
5097
|
+
* Rename a grouping field in every class exposing it. A blank name, or a
|
|
5098
|
+
* name that a column of any experimental class has, changes nothing: the
|
|
5099
|
+
* name is the field's identity across classes, and the cards merge by it.
|
|
5100
|
+
*/
|
|
5101
|
+
function renameGroupByField(fieldKey, name) {
|
|
5102
|
+
const next = name.trim();
|
|
5103
|
+
const targets = schemasExposing(fieldKey);
|
|
5104
|
+
if (!next || next === fieldKey || targets.length === 0) return;
|
|
5105
|
+
if (Object.values(schemas.value).some((schema) => isExperimentalKind(schema.classKind) && schema.columns.some((c) => (c.displayName ?? c.name) === next))) return;
|
|
5106
|
+
for (const { key, index } of targets) setColumnName(key, index, next);
|
|
5107
|
+
}
|
|
5108
|
+
/** Number of experimental groups that remain when the field is switched off in every class. */
|
|
5109
|
+
function groupCountWithout(fieldKey) {
|
|
5110
|
+
const targets = new Map(schemasExposing(fieldKey).map(({ key, index }) => [key, index]));
|
|
5111
|
+
return composeWith(Object.fromEntries(Object.entries(schemas.value).map(([key, schema]) => [key, targets.has(key) ? {
|
|
5112
|
+
...schema,
|
|
5113
|
+
groupBy: schema.groupBy.filter((i) => i !== targets.get(key))
|
|
5114
|
+
} : schema]))).experimentalGroups?.length ?? 0;
|
|
5115
|
+
}
|
|
5116
|
+
/**
|
|
4985
5117
|
* Move a grouping field in the active schema, then project that visible field
|
|
4986
5118
|
* order across every class. Class-only fields keep their relative order after
|
|
4987
5119
|
* the shared fields because the Field Recipe has no card with which to place
|
|
@@ -5176,6 +5308,7 @@ function useAutoGroup() {
|
|
|
5176
5308
|
schemas.value = {};
|
|
5177
5309
|
activeClassKey.value = "";
|
|
5178
5310
|
tokenized.value = [];
|
|
5311
|
+
experiment.value = {};
|
|
5179
5312
|
outliers.value = [];
|
|
5180
5313
|
sampleTypeHints.value = [];
|
|
5181
5314
|
}
|
|
@@ -5188,6 +5321,7 @@ function useAutoGroup() {
|
|
|
5188
5321
|
schemas,
|
|
5189
5322
|
activeClassKey,
|
|
5190
5323
|
delimiter,
|
|
5324
|
+
experiment,
|
|
5191
5325
|
dominantFieldCount,
|
|
5192
5326
|
minFieldCount,
|
|
5193
5327
|
samples,
|
|
@@ -5215,6 +5349,8 @@ function useAutoGroup() {
|
|
|
5215
5349
|
moveGroupBy,
|
|
5216
5350
|
toggleGroupByField,
|
|
5217
5351
|
moveGroupByField,
|
|
5352
|
+
renameGroupByField,
|
|
5353
|
+
groupCountWithout,
|
|
5218
5354
|
setRole,
|
|
5219
5355
|
setColumnName,
|
|
5220
5356
|
setValueOps,
|
|
@@ -7858,6 +7994,6 @@ function pickerFormat(name) {
|
|
|
7858
7994
|
return compound ? `${compound[1]}.gz` : name.includes(".") ? name.split(".").pop() : "";
|
|
7859
7995
|
}
|
|
7860
7996
|
//#endregion
|
|
7861
|
-
export {
|
|
7997
|
+
export { downloadBlob as $, parseDelimitedText as A, useAppExperiment as At, GROUPING_FIELD_ROLES as B, useTextSearch as Bt, useExperimentSamples as C, defineControlModel as Ct, getSampleGroupMajorPrefix as D, controlValuesToComponentProps as Dt, useSampleGroups as E, controlValuesToComponentBindingsById as Et, useAutoGroup as F, usePlatformContext as Ft, reorderList as G, classKey as H, useSortedItems as Ht, extractSampleNamesFromDesignData as I, useTheme as It, useDoseCalculator as J, useListReorder as K, extractSampleOptionsFromDesignData as L, useToast as Lt, useFileImport as M, useRuntimeAlignment as Mt, validateImportFile as N, getPresenceClientId as Nt, getSampleGroupParentPath as O, mergeControlWorkspaceOptions as Ot, extractSamplesFromDesignData as P, usePresenceHeartbeat as Pt, createPluginClient as Q, unwrapExperimentDesignData as R, candidateMatchesSearch as Rt, useScheduleDrag as S, useControlWorkspace as St, useExpansionSet as T, controlValuesToComponentBindings as Tt, useRackEditor as U, useRovingFocus as Ut, isExperimentalKind as V, compareSortValues as Vt, REORDER_ANNOUNCER_STYLE as W, useJobsStatusTray as X, combinePluginJobSources as Y, usePluginJobCenter as Z, toBioTemplateComponentPropsByComponent as _, defineDoseCalculatorControlProps as _t, useExperimentSelector as a, usePluginClient as at, useTemplateCollection as b, defineWellPlateDoseComponentBindings as bt, DEFAULT_PRESETS as c, buildPluginEndpointUrl as ct, useReagentSeries as d, usePluginWorkspace as dt, downloadPluginEndpoint as et, useGroupAssignment as f, DEFAULT_MOBILE_VIEWPORT_QUERY as ft, getBioTemplateComponentProps as g, useForm as gt, useBioTemplateWorkspace as h, evaluateCondition as ht, useAnalysisArtifacts as i, useCurrentExperiment as it, readFileAsText as j, getInjectedFrontendRevision as jt, detectDelimiter as k, APP_EXPERIMENT_KEY as kt, DEFAULT_UNITS as l, resolvePluginBaseUrl as lt, useBioTemplatePackWorkspace as m, useFormBuilder as mt, pickerFormat as n, pluginFormDataFromPayload as nt, useDebouncedWatch as o, usePluginEventStream as ot, useBioTemplatePresetWorkspace as p, useMobileSupportGate as pt, useReorderAnnouncer as q, pickerReason as r, uploadPluginEndpoint as rt, useProtocolTemplates as s, usePluginSettings as st, pickerExclusion as t, getPluginPageSelectorItems as tt, generateDilutionSeries as u, useRequestSyncState as ut, useBioTemplateComponents as v, defineDoseDesignControlModel as vt, useExperimentData as w, resolveControlModel as wt, useExperimentSave as x, defineWellPlateDoseControlProps as xt, useBioTemplateControls as y, defineWellPlateControlProps as yt, DEFAULT_COLORS as z, normalizeSearchQuery as zt };
|
|
7862
7998
|
|
|
7863
|
-
//# sourceMappingURL=validation-
|
|
7999
|
+
//# sourceMappingURL=validation-CukfLjy-.js.map
|