diff --git a/client/mass/about.ts b/client/mass/about.ts index 171ab37fc7..ed1a4133f0 100644 --- a/client/mass/about.ts +++ b/client/mass/about.ts @@ -181,6 +181,20 @@ export class MassAbout { lst: [] } }) + if (clearOnChange.groups) { + for (const group of state.groups) { + subactions.push({ + type: 'delete_group', + name: group.name + }) + } + for (const term of state.customTerms) { + subactions.push({ + type: 'delete_customTerm', + name: term.name + }) + } + } subactions.push({ type: 'cohort_set', activeCohort: i }) app.dispatch({ type: 'app_refresh', @@ -227,6 +241,7 @@ export class MassAbout { if (!result.cfeatures.length) return for (const feature of result.features) rows.push([{ value: feature.name }]) for (const cohort of result.cohorts) { + if (cohort.subcohorts?.length) continue columns.push({ label: cohort.name }) for (const [i, feature] of result.features.entries()) { const cf = result.cfeatures.find(cf => cf.idfeature === feature.idfeature && cf.cohort === cohort.cohort) diff --git a/client/mass/store.js b/client/mass/store.js index d8041efb7b..bb6528f18b 100644 --- a/client/mass/store.js +++ b/client/mass/store.js @@ -97,6 +97,7 @@ class TdbStore { let plot try { const _ = await import(`../plots/${savedPlot.chartType}.js`) + // this.state{} is already fully set with initial state, thus okay to pass to getPlotConfig() plot = await _.getPlotConfig(savedPlot, this.app, this.state.activeCohort) } catch (e) { this.app.printError(e) diff --git a/client/plots/genomeBrowser.controls.js b/client/plots/genomeBrowser.controls.js index e4adba61f5..54e3e36e97 100644 --- a/client/plots/genomeBrowser.controls.js +++ b/client/plots/genomeBrowser.controls.js @@ -1,8 +1,6 @@ import { getCompInit } from '#rx' -import { Menu } from '#dom/menu' -import { make_one_checkbox } from '#dom/checkbox' +import { Menu, Tabs, make_one_checkbox } from '#dom' import { filterInit, getNormalRoot, getFilterItemByTag } from '#filter/filter' -import { Tabs } from '../dom/toggleButtons' import { appInit } from '#termdb/app' /* @@ -13,7 +11,6 @@ main render1group makePrompt2addNewGroup launchMenu_createGroup - mayGetActiveCohortIdx render1group_info render1group_population render1group_filter @@ -51,6 +48,7 @@ class GbControls { //delete this._partialData return { + activeCohort: appState.activeCohort, config, termdbConfig: appState.termdbConfig, filter: getNormalRoot(appState.termfilter.filter) @@ -395,7 +393,6 @@ async function render1group_filter(self, groupIdx, group, div) { when initiating the filter ui, must join group's filter with mass global filter and submit the joined filter to main() this allows tvs edit to show correct number of samples */ - let span if (!self.filterUI[groupIdx]) { self.filterUI[groupIdx] = await filterInit({ @@ -580,7 +577,10 @@ function launchMenu_createGroup(self, groupIdx, div) { const arg = { holder: tab.contentHolder, vocabApi: self.app.vocabApi, - state: { termfilter: { filter: self.state.filter } }, + state: { + activeCohort: self.state.activeCohort, + termfilter: { filter: self.state.filter } + }, tree: { click_term2select_tvs: tvs => { ///////////////////////////////// @@ -605,8 +605,6 @@ function launchMenu_createGroup(self, groupIdx, div) { } } } - const activeCohortIdx = mayGetActiveCohortIdx(self) - if (Number.isInteger(activeCohortIdx)) arg.state.activeCohort = activeCohortIdx appInit(arg) continue } @@ -637,20 +635,3 @@ export function mayUpdateGroupTestMethodsIdx(self, d) { // otherwise, do not change existing method idx } } - -// from mass filter, find a tvs as cohortFilter, to know its array index in selectCohort.values[] -// return undefined for anything that's not valid -function mayGetActiveCohortIdx(self) { - if (!self.state.config.filter) return // no mass filter - const cohortFilter = getFilterItemByTag(self.config.filter, 'cohortFilter') // the tvs object - if (cohortFilter && self.state.termdbConfig.selectCohort) { - // the tvs is found - const cohortName = cohortFilter.tvs.values - .map(d => d.key) - .sort() - .join(',') - const idx = self.state.termdbConfig.selectCohort.values.findIndex(v => v.keys.sort().join(',') == cohortName) - if (idx == -1) throw 'subcohort key is not in selectCohort.values[]' - return idx - } -} diff --git a/client/plots/genomeBrowser.js b/client/plots/genomeBrowser.js index d6265b3e6a..5ab5b67d02 100644 --- a/client/plots/genomeBrowser.js +++ b/client/plots/genomeBrowser.js @@ -38,15 +38,7 @@ this{} terms[] geneSearchResult{} snvindel {} - details {} - groupTypes[] - groups:[] - // each element is a group object - {type='info', infoKey=str} - {type='filter', filter={}} - {type='population', key, label, ..} - groupTestMethod{} - groupTestMethodsIdx + details {} // see type def populations [{key,label}] // might not be part of state ld {} tracks[] @@ -76,7 +68,7 @@ class genomeBrowser { this.type = 'genomeBrowser' } - async init() { + async init(appState) { const holder = this.opts.holder.append('div') this.opts.header .append('div') @@ -430,10 +422,11 @@ export const genomeBrowserInit = getCompInit(genomeBrowser) // this alias will allow abstracted dynamic imports export const componentInit = genomeBrowserInit -export async function getPlotConfig(opts, app) { +export async function getPlotConfig(opts, app, activeCohort) { + // 3rd arg is initial active cohort try { // request default queries config from dataset, and allows opts to override - return await getDefaultConfig(app.vocabApi, opts) + return await getDefaultConfig(app.vocabApi, opts, activeCohort) } catch (e) { throw `${e} [genomeBrowser getPlotConfig()]` } @@ -471,7 +464,7 @@ export function makeChartBtnMenu(holder, chartsInstance) { // must do this as 'plot_prep' does not call getPlotConfig() // request default queries config from dataset, and allows opts to override // this config{} will become this.state.config{} - const config = await getDefaultConfig(chartsInstance.app.vocabApi) + const config = await getDefaultConfig(chartsInstance.app.vocabApi, null, chartsInstance.state.activeCohort) config.chartType = 'genomeBrowser' config.geneSearchResult = result @@ -494,7 +487,7 @@ export function makeChartBtnMenu(holder, chartsInstance) { } // get default config of the app from vocabApi -async function getDefaultConfig(vocabApi, override) { +async function getDefaultConfig(vocabApi, override, activeCohort) { const config = await vocabApi.getMds3queryDetails() // request default variant filter (against vcf INFO) const vf = await vocabApi.get_variantFilter() @@ -506,6 +499,14 @@ async function getDefaultConfig(vocabApi, override) { // test method may be inconsistent with group configuration (e.g. no fisher for INFO fields), update test method here // 1st arg is a fake "self" mayUpdateGroupTestMethodsIdx({ state: { config: c2 } }, c2.snvindel.details) + // a type=filter group may use filterByCohort. in such case, modify default state to assign proper filter based on current cohort + const gf = c2.snvindel.details.groups.find(i => i.type == 'filter') + if (gf && gf.filterByCohort) { + // modify and assign + gf.filter = gf.filterByCohort[vocabApi.termdbConfig.selectCohort.values[activeCohort].keys.join(',')] + if (!gf.filter) throw 'unknown filter by current cohort name' + delete gf.filterByCohort + } } return c2 } diff --git a/server/src/mds3.init.js b/server/src/mds3.init.js index a143afb199..1b56cd674a 100644 --- a/server/src/mds3.init.js +++ b/server/src/mds3.init.js @@ -57,6 +57,7 @@ validate_query_snvindel gdc.validate_query_snvindel_byisoform gdc.validate_query_snvindel_byrange snvindelByRangeGetter_bcf + validateSampleHeader2 mayLimitSamples param2filter tid2value2filter @@ -705,13 +706,24 @@ function mayValidateSampleHeader(ds, samples, where) { function validateSampleHeader2(ds, samples, where) { const sampleIds = [] // ds?.cohort?.termdb.q.sampleName2id must be present + const unknownSamples = [] // samples present in big file header but missing from db for (const s of samples) { const id = ds.cohort.termdb.q.sampleName2id(s.name) - if (!Number.isInteger(id)) throw 'unknown sample name from ' + where + if (!Number.isInteger(id)) { + unknownSamples.push(s.name) + // TODO if file with unknown sample should still be usable, slot a mock element in sampleIds[]. downstream query should be able to ignore it + continue + } s.name = id sampleIds.push(s) } console.log(samples.length, 'samples from ' + where + ' of ' + ds.label) + if (unknownSamples.length) { + // unknown samples can be safely reported to server log + console.log('unknown samples: ' + unknownSamples.join(', ')) + // later attach a sanitized err msg to ds to report to client + throw 'unknown samples in big file' + } return sampleIds } diff --git a/server/src/mds3.variant.js b/server/src/mds3.variant.js index 7f5c34bff2..741033b381 100644 --- a/server/src/mds3.variant.js +++ b/server/src/mds3.variant.js @@ -78,7 +78,15 @@ function validateParam(q, ds) { if (q.details.groups.length > 2) throw 'q.details.groups[] has more than 2' for (const g of q.details.groups) { if (g.type == 'filter') { - if (typeof g.filter != 'object') throw '.filter not an object for group type=filter' + if (g.filter) { + if (typeof g.filter != 'object') throw '.filter not an object for group type=filter' + } else if (g.filterByCohort) { + if (!ds.cohort.termdb.selectCohort) throw 'filterByCohort in use but ds not using subcohort' + if (typeof g.filterByCohort != 'object') throw '.filterByCohort not an object for group type=filter' + // xx + } else { + throw 'unknown structure of group.type=filter' + } } else if (g.type == 'population') { if (!g.key) throw '.key missing from group type=population' if (!ds.queries.snvindel?.populations) throw 'group type=population but this ds does not have populations' diff --git a/server/test/tp/files/hg38/TermdbTest/db b/server/test/tp/files/hg38/TermdbTest/db index 6c4607a8f8..47b7d5e404 100644 Binary files a/server/test/tp/files/hg38/TermdbTest/db and b/server/test/tp/files/hg38/TermdbTest/db differ diff --git a/shared/types/src/dataset.ts b/shared/types/src/dataset.ts index 9becd809b1..066aaa2125 100644 --- a/shared/types/src/dataset.ts +++ b/shared/types/src/dataset.ts @@ -243,6 +243,72 @@ type Population = { sets: PopulationINFOset[] } +/** primarily for prebuilding germline genetic association for survivorship portal +accessible to client via termdb.js?for=mds3queryDetails +part of state of genomeBrowser plot +allowing for user modification +*/ +type SnvindelComputeDetails = { + /** in each element, type corresponds to same key in groups[] + used for rendering choices in group data types; but content is read-only and should not be part of state + */ + groupTypes: { + type: string + name: string + }[] + /** a type of computing decides numeric values for each variant displayed in tk + computing type is also determined by number of groups + if only 1 group: + type=info: use numeric info field + type=filter: use AF + type=population: use AF + if there're two groups: + both types are "filter": allow AF diff or fisher + "filter" and "population": allow AF diff or fisher + else: value difference + */ + groups: (SnvindelComputeGroup_filter | SnvindelComputeGroup_population | SnvindelComputeGroup_info)[] + /** define lists of group-comparison methods to compute one numerical value per variant + */ + groupTestMethods: { + /** method name. used both for display and identifier. cannot supply hardcoded values here as breaks tsc */ + name: string + /** optional custom text to put on mds3 tk y axis */ + axisLabel?: string + }[] + /** array index of groupTestMethods[] */ + groupTestMethodsIdx: number +} +/** supplies a pp filter (or filter by cohort) to restrict to a subset of samples from which to compute AF for each variant. +the filter will be user-modifiable +*/ +type SnvindelComputeGroup_filter = { + // FIXME type value can only be 'filter' but breaks tsc + type: string //'filter' + /** a given filter applied to all cohorts */ + //filter?: object + /** filter per cohort. use either filter or filterByCohort */ + filterByCohort?: { [key: string]: object } +} +/** a choice from snvindel.populations[] + */ +type SnvindelComputeGroup_population = { + type: string //'population' + /** used to identify corresponding population element */ + key: string + /** redundant, should be copied over from snvindel.populations[] */ + label: string + /** if true, can adjust race. may copy over instead of duplicating? */ + allowto_adjust_race: boolean + /** if true, race adjustion is being applied */ + adjust_race: boolean +} +type SnvindelComputeGroup_info = { + type: string //'info' + /** numerical INFO field name from bcf, allows to retrieve numeric values for each variant in tk */ + infoKey: string +} + /** a data type under ds.queries{} */ type SnvIndelQuery = { forTrack?: boolean @@ -269,7 +335,7 @@ so that it can work for a termdb-less ds, e.g. clinvar, where termdbConfig canno } allowSNPs?: boolean vcfid4skewerName?: boolean - details?: any + details?: SnvindelComputeDetails } type SvFusion = {