diff --git a/CHANGELOG.md b/CHANGELOG.md index 850597a0b1..70c80bd318 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,15 @@ All notable changes to this project will be documented in this file. ## Unreleased +Features: +- GDC cohort-MAF tool: allow to customize output file columns +- Clicking matrix cell to show similar info table as hovering over the matrix cell. +- Mds3 track uses simpler radio buttons to toggle between view modes such as lollipop and occurrence + Fixes: - Gene exp clustering will display an alert msg to inform user that a map is not doable when there is just one gene +- display total number of mutations on disco plot +- Fix oncomatrix error: adding dictionary term from row group menu ## 2.38.1 diff --git a/client/dom/numericAxis.js b/client/dom/numericAxis.js index 2be26cf2e7..95dfdb89cf 100644 --- a/client/dom/numericAxis.js +++ b/client/dom/numericAxis.js @@ -1,5 +1,4 @@ import { keyupEnter } from '#src/client' -//import {make_radios} from './radiobuttons.js' /* configure numeric axis diff --git a/client/dom/radiobutton.js b/client/dom/radiobutton.js index 05cf6fd6ae..da9f557790 100644 --- a/client/dom/radiobutton.js +++ b/client/dom/radiobutton.js @@ -17,10 +17,12 @@ .inputName common Name of , use random number if not given */ + +let nameSuffix = 0 + export function make_radios(arg) { const { holder, options, callback, styles } = arg - let nameSuffix = 0 - const inputName = arg.inputName || 'dom-radio-' + nameSuffix++ + const inputName = arg.inputName || 'pp-dom-radio-' + nameSuffix++ const divs = holder .selectAll() diff --git a/client/dom/table2col.js b/client/dom/table2col.js index 69bf55c4be..5abdf11ddc 100644 --- a/client/dom/table2col.js +++ b/client/dom/table2col.js @@ -1,8 +1,53 @@ +/* +make a html table of two columns, for showing a list of key-value pairs. +1st column shows key in gray text, 2nd column shows value in black text, or arbitrary button/svg etc +as rows are added, as soon as table width exceeds a limit, it auto scrolls + +to create new table, do: + + const table = table2col({holder}) + +when a new row needs to be added, do: + + const [td1,td2] = table.addRow() + td1.text('Key') + td2.text('Value') + + +arg{} + .holder + .margin todo +*/ +export function table2col(arg) { + const scrollDiv = arg.holder.append('div').style('max-width', '80vw') + + const table = scrollDiv.append('table').style('margin', '5px 8px').attr('class', 'sja_simpletable') + return { + scrollDiv, + table, + addRow: () => { + if (table.node().offsetHeight > 500) { + scrollDiv + .style('height', '450px') + .style('resize', 'both') + .style('overflow-y', 'scroll') + .attr('class', 'sjpp_show_scrollbar') + } + const tr = table.append('tr') + const td1 = tr.append('td').style('padding', '3px').style('color', '#858585') + const td2 = tr.append('td') + return [td1, td2] + } + } +} + /* data[ {} ] .kvlst[] .k .v + +deprecated, replace with above */ export function make_table_2col(holder, data, overlen) { const color = '#9e9e9e' @@ -14,44 +59,22 @@ export function make_table_2col(holder, data, overlen) { for (const i of data) { const tr = table.append('tr') if (i.kvlst) { - tr.append('td') - .attr('rowspan', i.kvlst.length) - .style('padding', '3px') - .style('color', color) - .html(i.k) - tr.append('td') - .style('padding', '3px') - .style('color', color) - .html(i.kvlst[0].k) - tr.append('td') - .style('padding', '3px') - .html(i.kvlst[0].v) + tr.append('td').attr('rowspan', i.kvlst.length).style('padding', '3px').style('color', color).html(i.k) + tr.append('td').style('padding', '3px').style('color', color).html(i.kvlst[0].k) + tr.append('td').style('padding', '3px').html(i.kvlst[0].v) for (let j = 1; j < i.kvlst.length; j++) { const tr2 = table.append('tr') - tr2 - .append('td') - .style('padding', '3px') - .style('color', color) - .html(i.kvlst[j].k) - tr2 - .append('td') - .style('padding', '3px') - .html(i.kvlst[j].v) + tr2.append('td').style('padding', '3px').style('color', color).html(i.kvlst[j].k) + tr2.append('td').style('padding', '3px').html(i.kvlst[j].v) } } else { - tr.append('td') - .attr('colspan', 2) - .style('padding', '3px') - .style('color', color) - .html(i.k) + tr.append('td').attr('colspan', 2).style('padding', '3px').style('color', color).html(i.k) const td = tr.append('td').style('padding', '3px') if (overlen && i.v.length > overlen) { td.html(i.v.substr(0, overlen - 3) + ' ...»') .attr('class', 'sja_clbtext') .on('click', () => { - td.html(i.v) - .classed('sja_clbtext', false) - .on('click', null) + td.html(i.v).classed('sja_clbtext', false).on('click', null) }) } else { td.html(i.v) diff --git a/client/mass/app.js b/client/mass/app.js index 489de4d8fc..02f04bcbf2 100644 --- a/client/mass/app.js +++ b/client/mass/app.js @@ -126,7 +126,7 @@ class MassApp { const newPlots = {} let sandbox for (const plot of this.state.plots) { - if (!(plot.id in this.components.plots)) { + if (this.components.plots && !(plot.id in this.components.plots)) { sandbox = newSandboxDiv(this.dom.plotDiv, { close: () => { this.api.dispatch({ diff --git a/client/mass/test/barchart.integration.spec.js b/client/mass/test/barchart.integration.spec.js index 329160bf22..93b113af87 100644 --- a/client/mass/test/barchart.integration.spec.js +++ b/client/mass/test/barchart.integration.spec.js @@ -8,10 +8,13 @@ const vocabData = require('../../termdb/test/vocabData') const hideCategory = require('../../plots/barchart.events.js').hideCategory /* +TODO cover all combinations + Tests: - single barchart, categorical bars - single chart, with overlay - multiple charts + term1=categorical + term1=categorical, term2=defaultbins + term0=defaultbins, term1=categorical + term1=geneVariant series visibility - q.hiddenValue series visibility - numeric series visibility - condition @@ -55,7 +58,7 @@ tape('\n', function (test) { test.end() }) -tape('single barchart, categorical bars', function (test) { +tape('term1=categorical', function (test) { test.timeoutAfter(3000) runpp({ @@ -101,26 +104,16 @@ tape('single barchart, categorical bars', function (test) { } }) -tape('single chart, with overlay', function (test) { +tape('term1=categorical, term2=defaultbins', function (test) { test.timeoutAfter(5000) test.plan(4) - const termfilter = { filter: [] } runpp({ state: { - termfilter, plots: [ { chartType: 'barchart', term: { id: 'diaggrp' }, - term2: { id: 'agedx' }, - settings: { - controls: { - term2: { id: 'agedx', term: termjson['agedx'] } - }, - barchart: { - overlay: 'tree' - } - } + term2: { id: 'agedx' } } ] }, @@ -217,7 +210,7 @@ tape('single chart, with overlay', function (test) { } }) -tape('multiple charts', function (test) { +tape('term0=defaultbins, term1=categorical', function (test) { test.timeoutAfter(3000) runpp({ state: { @@ -225,15 +218,7 @@ tape('multiple charts', function (test) { { chartType: 'barchart', term: { id: 'diaggrp' }, - term0: { id: 'agedx' }, - settings: { - barchart: { - divideBy: 'tree' - }, - controls: { - term0: { id: 'agedx', term: termjson['agedx'] } - } - } + term0: { id: 'agedx' } } ] }, @@ -254,6 +239,34 @@ tape('multiple charts', function (test) { } }) +tape('term1=geneVariant', function (test) { + test.timeoutAfter(3000) + runpp({ + state: { + plots: [ + { + chartType: 'summary', // cannot use 'barchart', breaks + term: { term: { type: 'geneVariant', name: 'TP53' } } + } + ] + }, + barchart: { + callbacks: { + 'postRender.test': testNumCharts + } + } + }) + + let barDiv + function testNumCharts(barchart) { + barDiv = barchart.Inner.dom.barDiv + const numCharts = barDiv.selectAll('.pp-sbar-div').size() + test.true(numCharts > 2, 'should have more than 2 charts by TP53') + if (test._ok) barchart.Inner.app.destroy() + test.end() + } +}) + tape('series visibility - q.hiddenValues', function (test) { test.timeoutAfter(5000) test.plan(2) diff --git a/client/mass/test/regression.integration.spec.js b/client/mass/test/regression.integration.spec.js index 56cebbbca5..cf289d1591 100644 --- a/client/mass/test/regression.integration.spec.js +++ b/client/mass/test/regression.integration.spec.js @@ -24,14 +24,14 @@ const runpp = helpers.getRunPp('mass', { state: { nav: { header_mode: 'hide_search', - activeTab: 1, + activeTab: 1 }, vocab: { dslabel: 'TermdbTest', - genome: 'hg38-test', - }, + genome: 'hg38-test' + } }, - debug: 1, + debug: 1 }) async function getData(regression) { @@ -42,7 +42,7 @@ async function getData(regression) { const opts = { regressionType: c.regressionType, outcome: c.outcome, - independent: c.independent, + independent: c.independent } opts.filter = regression.Inner.inputs.parent.filter @@ -55,11 +55,11 @@ async function getData(regression) { function checkTableRow(table, idx, dataArray) { const checkArray = [] let issuesFound = 0 - table[idx].childNodes.forEach((t) => { + table[idx].childNodes.forEach(t => { checkArray.push(t.innerText) }) - dataArray.forEach((d) => { - if (!checkArray.some((t) => t == d)) { + dataArray.forEach(d => { + if (!checkArray.some(t => t == d)) { ++issuesFound } }) @@ -70,11 +70,11 @@ function checkTableRow(table, idx, dataArray) { function checkOnlyRowValues(valueNodes, dataArray) { const checkArray = [] let issuesFound = 0 - valueNodes.forEach((t) => { + valueNodes.forEach(t => { checkArray.push(t.innerText) }) - dataArray.forEach((d) => { - if (!checkArray.some((t) => t == d)) { + dataArray.forEach(d => { + if (!checkArray.some(t => t == d)) { ++issuesFound } }) @@ -86,12 +86,12 @@ function checkOnlyRowValues(valueNodes, dataArray) { test sections ***************/ -tape('\n', (test) => { +tape('\n', test => { test.pass('-***- plots/regression -***-') test.end() }) -tape('Linear: continuous outcome = "agedx", cat. independents = "sex" + "genetic_race"', (test) => { +tape('Linear: continuous outcome = "agedx", cat. independents = "sex" + "genetic_race"', test => { test.timeoutAfter(5000) runpp({ @@ -101,17 +101,17 @@ tape('Linear: continuous outcome = "agedx", cat. independents = "sex" + "genetic chartType: 'regression', regressionType: 'linear', outcome: { - id: 'agedx', + id: 'agedx' }, - independent: [{ id: 'sex' }, { id: 'genetic_race' }], - }, - ], + independent: [{ id: 'sex' }, { id: 'genetic_race' }] + } + ] }, regression: { callbacks: { - 'postRender.test': runTests, - }, - }, + 'postRender.test': runTests + } + } }) async function runTests(regression) { @@ -119,12 +119,12 @@ tape('Linear: continuous outcome = "agedx", cat. independents = "sex" + "genetic const regDom = regression.Inner.dom //**** Inputs **** - test.ok(regDom.inputs.node().querySelector('#sjpp-vp-violinDiv'), `Should render violin plot for outcome variable`) + test.ok(regDom.inputs.node().querySelector('.sjpp-vp-violinDiv'), `Should render violin plot for outcome variable`) test.equal( regDom.inputs .selectAll('table') .nodes() - .filter((t) => t.childNodes.length > 1).length, + .filter(t => t.childNodes.length > 1).length, 2, `Should render two tables for independent variables` ) @@ -136,7 +136,7 @@ tape('Linear: continuous outcome = "agedx", cat. independents = "sex" + "genetic const sampleSizeDiv = regDom.results .selectAll('div[name^="Sample size"] span') .nodes() - .filter((d) => d.innerText == data.sampleSize) + .filter(d => d.innerText == data.sampleSize) test.ok( regDom.results.node().querySelector('div[name^="Sample size"]') && sampleSizeDiv, `Should render "Sample size: ${data.sampleSize}"` @@ -153,7 +153,7 @@ tape('Linear: continuous outcome = "agedx", cat. independents = "sex" + "genetic table = regDom.results.selectAll('div[name^="Coefficients"] table tr').nodes() results = checkTableRow(table, 0, data.coefficients.header) test.equal(results, true, `Should render all coefficient headers in ${tableLabel}`) - const linearHeaders = data.coefficients.header.filter((d) => d === 'Beta' || d === 't value') + const linearHeaders = data.coefficients.header.filter(d => d === 'Beta' || d === 't value') test.equal(linearHeaders.length, 2, `Should render headers specific to linear regression`) results = checkTableRow(table, 1, data.coefficients.intercept) @@ -161,19 +161,19 @@ tape('Linear: continuous outcome = "agedx", cat. independents = "sex" + "genetic testTerm = 'Sex' const checkValues1 = ['Sex\nREF Female', 'Male'] - data.coefficients.terms.sex.categories[1].forEach((d) => checkValues1.push(d)) + data.coefficients.terms.sex.categories[1].forEach(d => checkValues1.push(d)) results = checkTableRow(table, 2, checkValues1) test.equal(results, true, `Should render all ${testTerm} data in ${tableLabel}`) testTerm = 'African Ancestry' const checkValues2 = ['Genetically defined race\nREF European Ancestry', testTerm] - data.coefficients.terms.genetic_race.categories[testTerm].forEach((d) => checkValues2.push(d)) + data.coefficients.terms.genetic_race.categories[testTerm].forEach(d => checkValues2.push(d)) results = checkTableRow(table, 3, checkValues2) test.equal(results, true, `Should render all ${testTerm} data in ${tableLabel}`) testTerm = 'Asian Ancestry' const checkValues3 = [testTerm] - data.coefficients.terms.genetic_race.categories[testTerm].forEach((d) => checkValues3.push(d)) + data.coefficients.terms.genetic_race.categories[testTerm].forEach(d => checkValues3.push(d)) results = checkTableRow(table, 4, checkValues3) test.equal(results, true, `Should render all ${testTerm} data in ${tableLabel}`) @@ -188,13 +188,13 @@ tape('Linear: continuous outcome = "agedx", cat. independents = "sex" + "genetic testTerm = 'Sex' const checkValues4 = [testTerm] - data.type3.terms.sex.forEach((d) => checkValues4.push(d)) + data.type3.terms.sex.forEach(d => checkValues4.push(d)) results = checkTableRow(table, 2, checkValues4) test.equal(results, true, `Should render all ${testTerm} data in ${tableLabel}`) testTerm = 'Genetically defined race' const checkValues5 = [testTerm] - data.type3.terms.genetic_race.forEach((d) => checkValues5.push(d)) + data.type3.terms.genetic_race.forEach(d => checkValues5.push(d)) results = checkTableRow(table, 3, checkValues5) test.equal(results, true, `Should render all ${testTerm} data in ${tableLabel}`) @@ -211,7 +211,7 @@ tape('Linear: continuous outcome = "agedx", cat. independents = "sex" + "genetic } }) -tape('Linear: continuous outcome = "agedx", continuous independent = "aaclassic_5"', (test) => { +tape('Linear: continuous outcome = "agedx", continuous independent = "aaclassic_5"', test => { test.timeoutAfter(3000) runpp({ @@ -221,17 +221,17 @@ tape('Linear: continuous outcome = "agedx", continuous independent = "aaclassic_ chartType: 'regression', regressionType: 'linear', outcome: { - id: 'agedx', + id: 'agedx' }, - independent: [{ id: 'aaclassic_5', q: { mode: 'continuous' } }], - }, - ], + independent: [{ id: 'aaclassic_5', q: { mode: 'continuous' } }] + } + ] }, regression: { callbacks: { - 'postRender.test': runTests, - }, - }, + 'postRender.test': runTests + } + } }) async function runTests(regression) { @@ -240,7 +240,7 @@ tape('Linear: continuous outcome = "agedx", continuous independent = "aaclassic_ //**** Inputs **** test.equal( - regDom.inputs.selectAll('#sjpp-vp-violinDiv').nodes().length, + regDom.inputs.selectAll('.sjpp-vp-violinDiv').nodes().length, 2, `Should render violin plot for outcome variable` ) @@ -273,7 +273,7 @@ tape('Linear: continuous outcome = "agedx", continuous independent = "aaclassic_ } }) -tape('Linear: continuous outcome = "agedx", discrete independent = "aaclassic_5"', (test) => { +tape('Linear: continuous outcome = "agedx", discrete independent = "aaclassic_5"', test => { test.timeoutAfter(5000) runpp({ @@ -283,17 +283,17 @@ tape('Linear: continuous outcome = "agedx", discrete independent = "aaclassic_5" chartType: 'regression', regressionType: 'linear', outcome: { - id: 'agedx', + id: 'agedx' }, - independent: [{ id: 'aaclassic_5', q: { mode: 'discrete' } }], - }, - ], + independent: [{ id: 'aaclassic_5', q: { mode: 'discrete' } }] + } + ] }, regression: { callbacks: { - 'postRender.test': runTests, - }, - }, + 'postRender.test': runTests + } + } }) async function runTests(regression) { @@ -333,7 +333,7 @@ tape('Linear: continuous outcome = "agedx", discrete independent = "aaclassic_5" } }) -tape('Linear: continuous outcome = "agedx", cubic spline independent = "aaclassic_5"', (test) => { +tape('Linear: continuous outcome = "agedx", cubic spline independent = "aaclassic_5"', test => { test.timeoutAfter(9000) // increased to 9 seconds to avoid timeout runpp({ @@ -344,19 +344,19 @@ tape('Linear: continuous outcome = "agedx", cubic spline independent = "aaclassi regressionType: 'linear', outcome: { id: 'agedx', - isAtomic: true, + isAtomic: true }, independent: [ - { id: 'aaclassic_5', q: { mode: 'spline', knots: [{ value: 2000 }, { value: 12000 }, { value: 24000 }] } }, - ], - }, - ], + { id: 'aaclassic_5', q: { mode: 'spline', knots: [{ value: 2000 }, { value: 12000 }, { value: 24000 }] } } + ] + } + ] }, regression: { callbacks: { - 'postRender.test': runTests, - }, - }, + 'postRender.test': runTests + } + } }) async function runTests(regression) { @@ -365,7 +365,7 @@ tape('Linear: continuous outcome = "agedx", cubic spline independent = "aaclassi const numOfKnots = regression.Inner.state.config.independent[0].q.knots.length //**** Inputs **** - const knotLines = regDom.inputs.selectAll('#sjpp-vp-violinDiv .sjpp-vp-line').nodes() + const knotLines = regDom.inputs.selectAll('.sjpp-vp-violinDiv .sjpp-vp-line').nodes() test.equal(knotLines.length, numOfKnots, `Should render 3 lines over the independent variable violin plot`) //**** Results **** @@ -410,7 +410,7 @@ tape('Linear: continuous outcome = "agedx", cubic spline independent = "aaclassi } }) -tape('Logistic: binary outcome = "hrtavg", continuous independent = "agedx"', (test) => { +tape('Logistic: binary outcome = "hrtavg", continuous independent = "agedx"', test => { test.timeoutAfter(3000) runpp({ @@ -421,17 +421,17 @@ tape('Logistic: binary outcome = "hrtavg", continuous independent = "agedx"', (t regressionType: 'logistic', outcome: { id: 'hrtavg', - isAtomic: true, + isAtomic: true }, - independent: [{ id: 'agedx' }], - }, - ], + independent: [{ id: 'agedx' }] + } + ] }, regression: { callbacks: { - 'postRender.test': runTests, - }, - }, + 'postRender.test': runTests + } + } }) async function runTests(regression) { @@ -445,7 +445,7 @@ tape('Logistic: binary outcome = "hrtavg", continuous independent = "agedx"', (t const sampleSizeDiv = regDom.results .selectAll('div[name^="Sample size"] span') .nodes() - .filter((d) => d.innerText == data.sampleSize) + .filter(d => d.innerText == data.sampleSize) test.ok( regDom.results.node().querySelector('div[name^="Sample size"]') && sampleSizeDiv, `Should render "Sample size: ${data.sampleSize}"` @@ -462,7 +462,7 @@ tape('Logistic: binary outcome = "hrtavg", continuous independent = "agedx"', (t table = regDom.results.selectAll('div[name^="Coefficients"] table tr').nodes() results = checkTableRow(table, 0, data.coefficients.header) test.equal(results, true, `Should render all coefficient headers in ${tableLabel}`) - const logHeaders = data.coefficients.header.filter((d) => d === 'Odds ratio' || d === 'Log odds' || d === 'z value') + const logHeaders = data.coefficients.header.filter(d => d === 'Odds ratio' || d === 'Log odds' || d === 'z value') test.equal(logHeaders.length, 3, `Should render headers specific to logistic regression`) results = checkTableRow(table, 1, data.coefficients.intercept) @@ -470,7 +470,7 @@ tape('Logistic: binary outcome = "hrtavg", continuous independent = "agedx"', (t testTerm = 'Age (years) at Cancer Diagnosis' const checkValues1 = [testTerm, '(continuous)'] - data.coefficients.terms.agedx.fields.forEach((d) => checkValues1.push(d)) + data.coefficients.terms.agedx.fields.forEach(d => checkValues1.push(d)) results = checkTableRow(table, 2, checkValues1) test.equal(results, true, `Should render all ${testTerm} data in ${tableLabel}`) @@ -484,7 +484,7 @@ tape('Logistic: binary outcome = "hrtavg", continuous independent = "agedx"', (t test.equal(results, true, `Should render all intercept data in ${tableLabel}`) const checkValues2 = [testTerm] - data.type3.terms.agedx.forEach((d) => checkValues2.push(d)) + data.type3.terms.agedx.forEach(d => checkValues2.push(d)) results = checkTableRow(table, 2, checkValues2) test.equal(results, true, `Should render all ${testTerm} data in ${tableLabel}`) @@ -501,7 +501,7 @@ tape('Logistic: binary outcome = "hrtavg", continuous independent = "agedx"', (t } }) -tape('Cox: graded outcome = "Arrhythmias", discrete independent = "agedx"', (test) => { +tape('Cox: graded outcome = "Arrhythmias", discrete independent = "agedx"', test => { test.timeoutAfter(3000) runpp({ @@ -511,17 +511,17 @@ tape('Cox: graded outcome = "Arrhythmias", discrete independent = "agedx"', (tes chartType: 'regression', regressionType: 'cox', outcome: { - id: 'Arrhythmias', + id: 'Arrhythmias' }, - independent: [{ id: 'agedx' }], - }, - ], + independent: [{ id: 'agedx' }] + } + ] }, regression: { callbacks: { - 'postRender.test': runTests, - }, - }, + 'postRender.test': runTests + } + } }) async function runTests(regression) { @@ -537,7 +537,7 @@ tape('Cox: graded outcome = "Arrhythmias", discrete independent = "agedx"', (tes const sampleSizeDiv = regDom.results .selectAll('div[name^="Sample size"] span') .nodes() - .filter((d) => d.innerText == data.sampleSize) + .filter(d => d.innerText == data.sampleSize) test.ok( regDom.results.node().querySelector('div[name^="Sample size"]') && sampleSizeDiv, `Should render "Sample size: ${data.sampleSize}"` @@ -547,7 +547,7 @@ tape('Cox: graded outcome = "Arrhythmias", discrete independent = "agedx"', (tes const numOfEventsDiv = regDom.results .selectAll('div[name^="Number of events"] span') .nodes() - .filter((d) => d.innerText == data.eventCnt) + .filter(d => d.innerText == data.eventCnt) test.ok( regDom.results.node().querySelector('div[name^="Number of events"]') && numOfEventsDiv, `Should render "Number of events: ${data.eventCnt}"` @@ -563,12 +563,12 @@ tape('Cox: graded outcome = "Arrhythmias", discrete independent = "agedx"', (tes table = regDom.results.selectAll('div[name^="Coefficients"] table tr').nodes() results = checkTableRow(table, 0, data.coefficients.header) test.equal(results, true, `Should render all coefficient headers in ${tableLabel}`) - const coxHeaders = data.coefficients.header.filter((d) => d === 'HR' || d === 'z') + const coxHeaders = data.coefficients.header.filter(d => d === 'HR' || d === 'z') test.equal(coxHeaders.length, 2, `Should render headers specific to cox regression in ${tableLabel}`) testTerm = 'Age (years) at Cancer Diagnosis' const checkValues1 = [testTerm, '(continuous)'] - data.coefficients.terms.agedx.fields.forEach((d) => checkValues1.push(d)) + data.coefficients.terms.agedx.fields.forEach(d => checkValues1.push(d)) results = checkTableRow(table, 1, checkValues1) test.equal(results, true, `Should render all ${testTerm} data in ${tableLabel}`) @@ -579,7 +579,7 @@ tape('Cox: graded outcome = "Arrhythmias", discrete independent = "agedx"', (tes test.equal(results, true, `Should render all header data in ${tableLabel}`) const checkValues2 = [testTerm] - data.type3.terms.agedx.forEach((d) => checkValues2.push(d)) + data.type3.terms.agedx.forEach(d => checkValues2.push(d)) results = checkTableRow(table, 1, checkValues2) test.equal(results, true, `Should render all ${testTerm} data in ${tableLabel}`) diff --git a/client/mass/test/sampleScatter.integration.spec.js b/client/mass/test/sampleScatter.integration.spec.js index 24c2ac111f..ca92f05051 100644 --- a/client/mass/test/sampleScatter.integration.spec.js +++ b/client/mass/test/sampleScatter.integration.spec.js @@ -683,8 +683,8 @@ tape('Change symbol and reference size from menu', function (test) { async function runTests(scatter) { helpers .rideInit({ arg: scatter, bus: scatter, eventType: 'postRender.test' }) - .run(changeSymbolInput) - .run(testSymbolSize, { wait: 100 }) + .use(changeSymbolInput) + .to(testSymbolSize, { wait: 100 }) .use(changeRefInput, { wait: 100 }) .to(testRefDotSize, { wait: 300 }) .done(test) @@ -699,7 +699,7 @@ tape('Change symbol and reference size from menu', function (test) { } function testSymbolSize(scatter) { //separate function because wait needed before test to run - test.ok(scatter.Inner.settings.size == testSymSize, `Should change symbol dot size to test value = ${testSymSize}`) + test.equal(scatter.Inner.settings.size, testSymSize, `Should change symbol dot size to test value = ${testSymSize}`) } function changeRefInput(scatter) { const refInput = scatter.Inner.dom.controlsHolder diff --git a/client/mass/test/summary.integration.spec.js b/client/mass/test/summary.integration.spec.js index 6f4e54127c..bc91e55dae 100644 --- a/client/mass/test/summary.integration.spec.js +++ b/client/mass/test/summary.integration.spec.js @@ -1,7 +1,7 @@ import tape from 'tape' import * as helpers from '../../test/front.helpers.js' import { select, selectAll } from 'd3-selection' -import { detectOne, detectGte } from '../../test/test.helpers.js' +import { detectOne, detectGte, detectStyle } from '../../test/test.helpers.js' /* Tests: @@ -36,7 +36,7 @@ const runpp = helpers.getRunPp('mass', { test sections ***************/ -tape('\n', function(test) { +tape('\n', function (test) { test.pass('-***- plots/summary -***-') test.end() }) @@ -65,18 +65,16 @@ tape('Render summary plot, term: "agedx"', test => { async function runTests(summary) { summary.on('postRender.test', null) - const sandboxDom = summary.Inner.dom - testHeader(summary, sandboxDom) - await testToggleButtons(summary, sandboxDom) + testHeader(summary, summary.Inner.dom) + await testToggleButtons(summary, summary.Inner.dom) await testOrientation(summary) - if (test._ok) summary.Inner.app.destroy() test.end() } - function testHeader(summary, sandboxDom) { - const headerText = sandboxDom.paneTitleDiv.select('div.sjpp-term-header').node() + function testHeader(summary, dom) { + const headerText = dom.paneTitleDiv.select('div.sjpp-term-header').node() const configTerm = summary.Inner.config.term.term.name test.equal( headerText.innerHTML, @@ -85,8 +83,8 @@ tape('Render summary plot, term: "agedx"', test => { ) } - async function testToggleButtons(summary, sandboxDom) { - const toggles = sandboxDom.chartToggles + async function testToggleButtons(summary, dom) { + const toggles = dom.chartToggles .selectAll('div > div> button') .nodes() .filter(d => d.__data__.isVisible() == true) @@ -103,35 +101,41 @@ tape('Render summary plot, term: "agedx"', test => { test.equal(tabLabels2Find.length, foundLabels, `Should render tabs: ${tabLabels2Find}`) //Toggle to violin - toggles.find(d => d.__data__.childType == 'violin').click() const foundTestPlot = await detectOne({ elem: summary.Inner.dom.holder.body.node(), - selector: '#sjpp-vp-violinDiv' + selector: '.sjpp-violin-plot', + trigger() { + toggles.find(d => d.__data__.childType == 'violin').click() + } }) test.ok(foundTestPlot, `Should render violin after toggle`) test.equal(summary.Inner.state.config.childType, 'violin', `Should toggle to childType = violin`) - - //Toggle back to barchart - toggles.find(d => d.__data__.childType == 'barchart').click() - const foundOrigPlot = await detectOne({ elem: summary.Inner.dom.holder.body.node(), selector: '.pp-sbar-div' }) - test.ok(foundOrigPlot, `Should render barchart after toggle`) + // Toggle back to barchart + await detectStyle({ + elem: summary.Inner.dom.plotDivs.barchart.node(), + matcher(mutations) { + for (const m of mutations) { + if (m.attributeName == 'style' && m.target.style.display === '') return m + } + }, + trigger() { + toggles.find(d => d.__data__.childType == 'barchart').click() + } + }) test.equal(summary.Inner.state.config.childType, 'barchart', `Should toggle back to childType = 'barchart'`) } async function testOrientation(summary) { - summary.Inner.app.dispatch({ + await summary.Inner.app.dispatch({ type: 'plot_edit', id: summary.Inner.id, config: { settings: { barchart: { orientation: 'vertical' } } } }) - await detectGte({ elem: summary.Inner.dom.plotDivs.barchart.node(), selector: '.bars-collabels' }) test.notEqual( summary.Inner.config.settings.barchart.orientation, summary.Inner.config.settings.violin.orientation, - - summary.Inner.config.settings.barchart.orientation != summary.Inner.config.settings.violin.orientation, `Orientation change for barchart should not affect violin` ) } @@ -201,16 +205,14 @@ tape('Barchart & violin toggles, term: "diaggrp", term2: "agedx"', test => { async function runTests(summary) { summary.on('postRender.test', null) - const sandboxDom = summary.Inner.dom - - await testToggleButtons(summary, sandboxDom) - + await detectOne({ elem: summary.Inner.dom.holder.body.node(), selector: '.pp-bars-svg' }) + await testToggleButtons(summary, summary.Inner.dom) if (test._ok) summary.Inner.app.destroy() test.end() } - async function testToggleButtons(summary, sandboxDom) { - const toggles = sandboxDom.chartToggles + async function testToggleButtons(summary, dom) { + const toggles = dom.chartToggles .selectAll('div > div> button') .nodes() .filter(d => d.__data__.isVisible() == true) @@ -218,16 +220,24 @@ tape('Barchart & violin toggles, term: "diaggrp", term2: "agedx"', test => { //Toggle to violin toggles.find(d => d.__data__.childType == 'violin').click() const foundTestPlot = await detectOne({ - elem: summary.Inner.dom.holder.body.node(), - selector: '#sjpp-vp-violinDiv' + elem: dom.holder.body.node(), + selector: '.sjpp-violin-plot' }) test.ok(foundTestPlot, `Should render violin after toggle`) test.equal(summary.Inner.state.config.childType, 'violin', `Should toggle to childType = violin`) //Toggle back to barchart - toggles.find(d => d.__data__.childType == 'barchart').click() - const foundOrigPlot = await detectOne({ elem: summary.Inner.dom.holder.body.node(), selector: '.pp-sbar-div' }) - test.ok(foundOrigPlot, `Should render barchart after toggle`) + await detectStyle({ + elem: dom.plotDivs.barchart.node(), + matcher(mutations) { + for (const m of mutations) { + if (m.attributeName == 'style' && m.target.style.display === '') return m + } + }, + trigger() { + toggles.find(d => d.__data__.childType == 'barchart').click() + } + }) test.equal(summary.Inner.state.config.childType, 'barchart', `Should toggle back to childType = 'barchart'`) } }) @@ -277,15 +287,23 @@ tape('Barchart & violin toggles, term: "agedx", term2: "diaggrp"', test => { toggles.find(d => d.__data__.childType == 'violin').click() const foundTestPlot = await detectOne({ elem: summary.Inner.dom.holder.body.node(), - selector: '#sjpp-vp-violinDiv' + selector: '.sjpp-violin-plot' }) test.ok(foundTestPlot, `Should render violin after toggle`) test.equal(summary.Inner.state.config.childType, 'violin', `Should toggle to childType = violin`) //Toggle back to barchart - toggles.find(d => d.__data__.childType == 'barchart').click() - const foundOrigPlot = await detectOne({ elem: summary.Inner.dom.holder.body.node(), selector: '.pp-sbar-div' }) - test.ok(foundOrigPlot, `Should render barchart after toggle`) + await detectStyle({ + elem: summary.Inner.dom.plotDivs.barchart.node(), + matcher(mutations) { + for (const m of mutations) { + if (m.attributeName == 'style' && m.target.style.display === '') return m + } + }, + trigger() { + toggles.find(d => d.__data__.childType == 'barchart').click() + } + }) test.equal(summary.Inner.state.config.childType, 'barchart', `Should toggle back to childType = 'barchart'`) } }) @@ -335,15 +353,23 @@ tape('Barchart & violin toggles, term: "agedx", term2: "hrtavg"', test => { toggles.find(d => d.__data__.childType == 'violin').click() const foundTestPlot = await detectOne({ elem: summary.Inner.dom.holder.body.node(), - selector: '#sjpp-vp-violinDiv' + selector: '.sjpp-violin-plot' }) test.ok(foundTestPlot, `Should render violin after toggle`) test.equal(summary.Inner.state.config.childType, 'violin', `Should toggle to childType = violin`) //Toggle back to barchart - toggles.find(d => d.__data__.childType == 'barchart').click() - const foundOrigPlot = await detectOne({ elem: summary.Inner.dom.holder.body.node(), selector: '.pp-sbar-div' }) - test.ok(foundOrigPlot, `Should render barchart after toggle`) + await detectStyle({ + elem: summary.Inner.dom.plotDivs.barchart.node(), + matcher(mutations) { + for (const m of mutations) { + if (m.attributeName == 'style' && m.target.style.display === '') return m + } + }, + trigger() { + toggles.find(d => d.__data__.childType == 'barchart').click() + } + }) test.equal(summary.Inner.state.config.childType, 'barchart', `Should toggle back to childType = 'barchart'`) } }) @@ -384,17 +410,15 @@ tape('Overlay continuity, term: "aaclassic_5", term2: "sex"', test => { } async function testOverlay(summary) { - const plotsConfig = summary.Inner.components.plots + const plots = summary.Inner.components.plots summary.Inner.dom.chartToggles .selectAll('div > div> button') .nodes() .find(d => d.__data__.childType == 'violin') .click() - await detectOne({ elem: summary.Inner.dom.holder.body.node(), selector: '#sjpp-vp-violinDiv' }) - test.equal( - plotsConfig.violin.Inner.config.term2.id, - testTerm, - `Overlay term = ${testTerm} carried over to violin plot` - ) + // NOTE: detect a rendered violin viz element, not the holder which may still be empty + // by the time test.equal is called below + await detectOne({ elem: summary.Inner.dom.holder.body.node(), selector: '.sjpp-violin-plot' }) + test.equal(plots.violin.Inner.config.term2.id, testTerm, `Overlay term = ${testTerm} carried over to violin plot`) } }) diff --git a/client/mds3/itemtable.js b/client/mds3/itemtable.js index 030035f3a4..6b72da8789 100644 --- a/client/mds3/itemtable.js +++ b/client/mds3/itemtable.js @@ -1,9 +1,9 @@ import { mclass, dtsnvindel, dtfusionrna, dtsv } from '#shared/common' import { init_sampletable } from './sampletable' -import { get_list_cells } from '#dom/gridutils' import { appear } from '#dom/animation' import { dofetch3 } from '#common/dofetch' import { renderTable } from '#dom/table' +import { table2col } from '#dom/table2col' /* itemtable @@ -94,26 +94,19 @@ function mayMoveTipDiv2left(arg) { display full details (and samples) for one item */ async function itemtable_oneItem(arg) { - const grid = arg.div.append('div').style('display', 'inline-grid').style('overflow-y', 'scroll') - - grid - .style('grid-template-columns', 'auto auto') - .style('max-height', '40vw') - // in case creating a new table for multiple samples of this variant, - // add space between grid and the new table - .style('margin-bottom', '10px') + const table = table2col({ holder: arg.div }) const m = arg.mlst[0] if (m.dt == dtsnvindel) { - table_snvindel(arg, grid) + table_snvindel(arg, table) } else { - await table_svfusion(arg, grid) + await table_svfusion(arg, table) } // if the variant has only one sample, - // allow to append new rows to grid to show sample key:value - arg.singleSampleDiv = grid + // allow to append new rows to table to show sample key:value + arg.singleSampleDiv = table // if there are multiple samples, this
won't be used // a new table will be created under arg.div to show sample table @@ -123,7 +116,7 @@ async function itemtable_oneItem(arg) { await init_sampletable(arg) } else { // invalid occurrence; still show row to indicate this - const [td1, td2] = get_list_cells(grid) + const [td1, td2] = table.addRow() td1.text('Occurrence') td2.text('occurrence' in m ? m.occurrence : '') } @@ -284,28 +277,28 @@ async function itemtable_multiItems(arg) { table display of variant attributes, for mlst[0] single variant do not show sample level details */ -function table_snvindel({ mlst, tk, block }, grid) { +function table_snvindel({ mlst, tk, block }, table) { const m = mlst[0] { - const [td1, td2] = get_list_cells(grid) + const [td1, td2] = table.addRow() td1.text(block.mclassOverride ? block.mclassOverride.className : 'Consequence') print_mname(td2, m) //add_csqButton(m, tk, td2, table) } { - const [td1, td2] = get_list_cells(grid) + const [td1, td2] = table.addRow() // do not pretend m is mutation if ref/alt is missing td1.text(m.ref && m.alt ? 'Mutation' : 'Position') print_snv(td2, m, tk) } if (m.occurrence > 1) { - const [td1, td2] = get_list_cells(grid) + const [td1, td2] = table.addRow() td1.text('Occurrence') td2.text(m.occurrence) } - table_snvindel_mayInsertNumericValueRow(m, tk, grid) - table_snvindel_mayInsertHtmlSections(m, tk, grid) - table_snvindel_mayInsertLD(m, tk, grid) + table_snvindel_mayInsertNumericValueRow(m, tk, table) + table_snvindel_mayInsertHtmlSections(m, tk, table) + table_snvindel_mayInsertLD(m, tk, table) if (m.info) { /* info fields are available for this variant @@ -319,7 +312,7 @@ function table_snvindel({ mlst, tk, block }, grid) { continue } - const [td1, td2] = get_list_cells(grid) + const [td1, td2] = table.addRow() // column 1: info field key td1.text(key) @@ -351,7 +344,7 @@ function table_snvindel({ mlst, tk, block }, grid) { } } -function table_snvindel_mayInsertNumericValueRow(m, tk, grid) { +function table_snvindel_mayInsertNumericValueRow(m, tk, table) { const currentMode = tk.skewer.viewModes.find(i => i.inuse) if (currentMode.type != 'numeric' || currentMode.byAttribute == 'occurrence') return // current mode is numeric and is not occurrence, as occurrence has already been shown in the table @@ -361,7 +354,7 @@ function table_snvindel_mayInsertNumericValueRow(m, tk, grid) { if (Array.isArray(tmp)) { for (const s of tmp) { // s should be {k,v} - const [td1, td2] = get_list_cells(grid) + const [td1, td2] = table.addRow() td1.text(s.k) td2.text(s.v) } @@ -371,15 +364,15 @@ function table_snvindel_mayInsertNumericValueRow(m, tk, grid) { return } - const [td1, td2] = get_list_cells(grid) + const [td1, td2] = table.addRow() td1.text(currentMode.label) td2.text(m.__value_missing ? 'NA' : m.__value_use) } -function table_snvindel_mayInsertHtmlSections(m, tk, grid) { +function table_snvindel_mayInsertHtmlSections(m, tk, table) { if (!m.htmlSections) return if (!Array.isArray(m.htmlSections)) throw 'htmlSections[] is not array' for (const section of m.htmlSections) { - const [td1, td2] = get_list_cells(grid) + const [td1, td2] = table.addRow() if (section.key && section.html) { td1.text(section.key) td2.html(section.html) @@ -509,22 +502,25 @@ function add_csqButton(m, tk, td, table) { } } -async function table_svfusion(arg, grid) { +async function table_svfusion(arg, table) { // display one svfusion event // svgraph in 1st row - grid.append('div') - await makeSvgraph(arg.mlst[0], grid.append('div').style('margin-bottom', '10px'), arg.block) + await makeSvgraph( + arg.mlst[0], + table.scrollDiv.insert('div', ':first-child'), // insert to top + arg.block + ) // rows { - const [c1, c2] = get_list_cells(grid) + const [c1, c2] = table.addRow() c1.text('Data type') c2.text(mclass[arg.mlst[0].class].label) } { // todo: support chimeric read fraction on each break end - const [c1, c2] = get_list_cells(grid) + const [c1, c2] = table.addRow() c1.text('Break points') for (const pair of arg.mlst[0].pairlst) { printSvPair(pair, c2.append('div')) @@ -587,9 +583,9 @@ async function getGm(p, block) { } } -function table_snvindel_mayInsertLD(m, tk, grid) { +function table_snvindel_mayInsertLD(m, tk, table) { if (!tk.mds.queries?.ld) return // not available - const [td1, td2] = get_list_cells(grid) + const [td1, td2] = table.addRow() td1.text('LD overlay') const m0 = tk.mds.queries.ld.mOverlay?.m diff --git a/client/mds3/leftlabel.sample.js b/client/mds3/leftlabel.sample.js index 9b646f3a00..7a7d2ea816 100644 --- a/client/mds3/leftlabel.sample.js +++ b/client/mds3/leftlabel.sample.js @@ -64,23 +64,35 @@ export function makeSampleFilterLabel(data, tk, block, laby) { tk.load() } } - if (block.usegm) { - ///////////////////////////////////// - // - // GDC specific logic - // in gene mode, supply the current gene name as a new parameter - // for the vocabApi getCategories() query, so it can pull the number of mutated samples for a term - // this parameter is used by some sneaky gdc-specific logic in termdb.matrix.js getData() - // should not impact non-gdc datasets - // - ///////////////////////////////////// - arg.getCategoriesArguments = { currentGeneNames: [block.usegm.name] } - // TODO {name: block.usegm.name, isoform, q:{allowedDt}} - } + mayAddGetCategoryArgs(arg, block) filterInit(arg).main(tk.filterObj) }) } +function mayAddGetCategoryArgs(arg, block) { + if (block.usegm) { + ///////////////////////////////////// + // + // GDC specific logic + // in gene mode, supply the current gene name as a new parameter + // for the vocabApi getCategories() query, so it can pull the number of mutated samples for a term + // this parameter is used by some sneaky gdc-specific logic in termdb.matrix.js getData() + // should not impact non-gdc datasets + // + ///////////////////////////////////// + arg.getCategoriesArguments = { currentGeneNames: [block.usegm.name] } + // TODO {name: block.usegm.name, isoform, q:{allowedDt}} + } else { + ///////////////////////////////////// + // + // GDC specific logic + // in genomic mode, pass rglst for pulling cases mutated in this region, handled in the same way as currentGeneNames + // + ///////////////////////////////////// + arg.getCategoriesArguments = { rglst: structuredClone(block.rglst) } + } +} + export function getFilterName(f) { // try to provide a meaningful name based on filter content diff --git a/client/mds3/leftlabel.variant.js b/client/mds3/leftlabel.variant.js index bcae37d294..fb8d7d26df 100644 --- a/client/mds3/leftlabel.variant.js +++ b/client/mds3/leftlabel.variant.js @@ -4,6 +4,7 @@ import { itemtable } from './itemtable' import { makelabel, positionLeftlabelg } from './leftlabel' import { to_textfile } from '#dom/downloadTextfile' import { Tabs } from '../dom/toggleButtons' +import { make_radios } from '../dom/radiobutton' import { rangequery_rglst } from './tk' import { samples2columnsRows, block2source } from './sampletable' import { dt2label, mclass, dtsnvindel, dtsv, dtfusionrna } from '#shared/common' @@ -89,6 +90,7 @@ function menu_variants(tk, block) { .append('div') .text('List') .attr('class', 'sja_menuoption') + .style('border-radius', '0px') .on('click', () => { listSkewerData(tk, block) }) @@ -97,6 +99,7 @@ function menu_variants(tk, block) { tk.menutip.d .append('div') .text('Cancel highlight') + .style('border-radius', '0px') .attr('class', 'sja_menuoption') .on('click', () => { delete tk.skewer.hlssmid @@ -124,6 +127,7 @@ function menu_variants(tk, block) { .append('div') .text('Collapse') .attr('class', 'sja_menuoption') + .style('border-radius', '0px') .on('click', () => { fold_glyph(tk.skewer.data, tk) tk.menutip.hide() @@ -133,6 +137,7 @@ function menu_variants(tk, block) { .append('div') .text('Expand') .attr('class', 'sja_menuoption') + .style('border-radius', '0px') .on('click', () => { settle_glyph(tk, block) tk.menutip.hide() @@ -145,6 +150,7 @@ function menu_variants(tk, block) { .append('div') .text(tk.skewer.hideDotLabels ? 'Show all variant labels' : 'Hide all variant labels') .attr('class', 'sja_menuoption') + .style('border-radius', '0px') .on('click', () => { tk.skewer.hideDotLabels = !tk.skewer.hideDotLabels tk.load() @@ -158,6 +164,7 @@ function menu_variants(tk, block) { .append('div') .text('Download') .attr('class', 'sja_menuoption') + .style('border-radius', '0px') .on('click', () => { downloadVariants(tk, block) tk.menutip.hide() @@ -239,28 +246,26 @@ function mayAddSkewerModeOption(tk, block) { return } // there are more than 1 mode, print name of current mode - tk.menutip.d - .append('div') - .style('margin', '10px 10px 3px 10px') - .style('font-size', '.7em') - .text(getViewmodeName(tk.skewer.viewModes.find(n => n.inuse))) - // show available modes - for (const n of tk.skewer.viewModes) { - if (n.inuse) continue - // a mode not in use; make option to switch to it - tk.menutip.d - .append('div') - .text(getViewmodeName(n)) - .attr('class', 'sja_menuoption') - .on('click', () => { - for (const i of tk.skewer.viewModes) i.inuse = false - n.inuse = true - tk.menutip.hide() - may_render_skewer({ skewer: tk.skewer.rawmlst }, tk, block) - positionLeftlabelg(tk, block) - tk._finish() - }) + const options = [] + for (const [idx, v] of tk.skewer.viewModes.entries()) { + const o = { + label: getViewmodeName(v), + value: idx + } + if (v.inuse) o.checked = true + options.push(o) } + make_radios({ + holder: tk.menutip.d.append('div').style('margin', '10px'), + options, + callback: async idx => { + for (const i of tk.skewer.viewModes) i.inuse = false + tk.skewer.viewModes[idx].inuse = true + may_render_skewer({ skewer: tk.skewer.rawmlst }, tk, block) + positionLeftlabelg(tk, block) + tk._finish() + } + }) } function getViewmodeName(n) { diff --git a/client/mds3/numericmode.js b/client/mds3/numericmode.js index d44ea3811a..340fdaa468 100644 --- a/client/mds3/numericmode.js +++ b/client/mds3/numericmode.js @@ -918,6 +918,7 @@ function render_axis(tk, nm, block) { .d.append('div') .text('Cancel') .attr('class', 'sja_menuoption') + .style('border-radius', '0px') .on('click', () => { tk.menutip.hide() nm.inuse = false diff --git a/client/mds3/sampletable.js b/client/mds3/sampletable.js index dba1bd455b..079ff18509 100644 --- a/client/mds3/sampletable.js +++ b/client/mds3/sampletable.js @@ -1,8 +1,8 @@ import { fillbar } from '#dom/fillbar' -import { get_list_cells } from '#dom/gridutils' import { mclass, dtsnvindel, dtsv, dtfusionrna } from '#shared/common' import { renderTable } from '#dom/table' import { newSandboxDiv } from '../dom/sandbox.ts' +import { table2col } from '../dom/table2col' import { rgb } from 'd3-color' import { print_snv, printSvPair } from './itemtable' import { convertUnits } from '#shared/helpers' @@ -159,21 +159,11 @@ async function feedSample2selectCallback(tk, block, _samples, sampleIdxLst) { } async function make_singleSampleTable(s, arg) { - const grid_div = - arg.singleSampleDiv || - arg.div - .append('div') - .style('display', 'inline-grid') - .style('grid-template-columns', 'auto auto') - .style('gap-row-gap', '1px') - .style('align-items', 'center') - .style('justify-items', 'left') - .style('padding', '10px') - .style('width', '100%') + const table = arg.singleSampleDiv || table2col({ holder: arg.div }) if (s.sample_id) { // sample_id is hardcoded - const [cell1, cell2] = get_list_cells(grid_div) + const [cell1, cell2] = table.addRow() cell1.text(arg.tk.mds.termdbConfig?.lollipop?.sample || 'Sample') printSampleName(s, arg.tk, cell2, arg.block, arg.mlst?.[0]) } @@ -181,14 +171,14 @@ async function make_singleSampleTable(s, arg) { ///////////// // hardcoded logic to represent if this case is open or controlled-access if ('caseIsOpenAccess' in s) { - const [cell1, cell2] = get_list_cells(grid_div) + const [cell1, cell2] = table.addRow() cell1.text('Access') cell2.text(s.caseIsOpenAccess ? 'Open' : 'Controlled') } if (arg.tk.mds.variant2samples.twLst) { for (const tw of arg.tk.mds.variant2samples.twLst) { - const [cell1, cell2] = get_list_cells(grid_div) + const [cell1, cell2] = table.addRow() cell1.text(tw.term.name).style('text-overflow', 'ellipsis') cell2.style('text-overflow', 'ellipsis') if (tw.id in s) { @@ -217,19 +207,21 @@ async function make_singleSampleTable(s, arg) { for (const ssmid of s.ssm_id_lst) { if (s.ssm_id_lst.length > 1) { // there are multiple, need to mark it out - const div = grid_div.append('div').style('grid-column', 'span 2').style('margin-top', '20px') + //const td = table.addRow({spanTwoColumns:true}) + const [td1, td] = table.addRow() + td.style('padding-top', '20px') const m = arg.tk.skewer.rawmlst.find(i => i.ssm_id == ssmid) if (m) { // found m object by id, can make a better display if (m.dt == 1) { - print_snv(div, m, arg.tk) + print_snv(td, m, arg.tk) } else if (m.dt == 2 || m.dt == 5) { - printSvPair(m.pairlst[0], div) + printSvPair(m.pairlst[0], td) } else { - div.text(ssmid) + td.text(ssmid) } } else { - div.text(ssmid) + td.text(ssmid) } } else { // only 1 ssm from this sample obj, no need to mark it out @@ -237,7 +229,7 @@ async function make_singleSampleTable(s, arg) { for (const formatkey in s.ssmid2format[ssmid]) { const value = s.ssmid2format[ssmid][formatkey] - const [cell1, cell2] = get_list_cells(grid_div) + const [cell1, cell2] = table.addRow() const fobj = arg.tk.mds?.bcf?.format?.[formatkey] cell1.text((fobj && fobj.Description) || formatkey) cell2.html(printFormat(fobj, value)) diff --git a/client/plots/barchart.js b/client/plots/barchart.js index c253a9d3ad..59d6ad2d11 100644 --- a/client/plots/barchart.js +++ b/client/plots/barchart.js @@ -245,7 +245,7 @@ class Barchart { this.term1toColor = {} this.term2toColor = {} // forget any assigned overlay colors when refreshing a barchart this.updateSettings(this.config) - this.colorScale = getColors(this.config.term.term2 ? this.settings.rows.length : this.settings.cols.length) + this.colorScale = getColors(this.config.term2 ? this.settings.cols.length : this.settings.rows.length) this.chartsData = this.processData(this.currServerData) this.render() diff --git a/client/plots/disco/DiscoRenderer.ts b/client/plots/disco/DiscoRenderer.ts index a29c3b9d2c..02cf366bfe 100644 --- a/client/plots/disco/DiscoRenderer.ts +++ b/client/plots/disco/DiscoRenderer.ts @@ -37,8 +37,11 @@ export class DiscoRenderer { controlsDiv, viewModel.settings.label.prioritizeGeneLabelsByGeneSets, viewModel.settings.label.showPrioritizeGeneLabelsByGeneSets, - viewModel.filteredSnvDataLength, - viewModel.svnDataLength, + viewModel.settings.label.prioritizeGeneLabelsByGeneSets && + viewModel.settings.label.showPrioritizeGeneLabelsByGeneSets + ? viewModel.filteredSnvDataLength + : viewModel.snvDataLength, + viewModel.snvDataLength, viewModel.genesetName ) diff --git a/client/plots/disco/data/DataMapper.ts b/client/plots/disco/data/DataMapper.ts index 3df314a995..25f51316b6 100644 --- a/client/plots/disco/data/DataMapper.ts +++ b/client/plots/disco/data/DataMapper.ts @@ -4,7 +4,7 @@ import DataObjectMapper from './DataObjectMapper.ts' import Settings from '#plots/disco/Settings.ts' import { ViewModelMapper } from '#plots/disco/viewmodel/ViewModelMapper.ts' import { DataHolder } from '#plots/disco/data/DataHolder.ts' -import { MutationTypes } from '#plots/disco/data/MutationTypes.ts' +import { dtsnvindel, dtfusionrna, dtsv, dtcnv, dtloh } from '#shared/common' export default class DataMapper { // remove fields and extract filters to seperate classes @@ -48,11 +48,11 @@ export default class DataMapper { private dataObjectMapper: DataObjectMapper private lastInnerRadious: number - private snvFilter = (data: Data) => data.dt == MutationTypes.SNV - private fusionFilter = (data: Data) => data.dt == MutationTypes.FUSION || data.dt == MutationTypes.SV + private snvFilter = (data: Data) => data.dt == dtsnvindel + private fusionFilter = (data: Data) => data.dt == dtfusionrna || data.dt == dtsv - private cnvFilter = (data: Data) => data.dt == MutationTypes.CNV - private lohFilter = (data: Data) => data.dt == MutationTypes.LOH + private cnvFilter = (data: Data) => data.dt == dtcnv + private lohFilter = (data: Data) => data.dt == dtloh private compareData = (a, b) => { const chrDiff = this.reference.chromosomesOrder.indexOf(a.chr) - this.reference.chromosomesOrder.indexOf(b.chr) @@ -73,8 +73,16 @@ export default class DataMapper { this.sample = sample this.lastInnerRadious = this.settings.rings.chromosomeInnerRadius - this.nonExonicFilter = (data: Data) => - settings.rings.nonExonicFilterValues.includes(ViewModelMapper.snvClassLayer[data.mClass]) + this.nonExonicFilter = (data: Data) => { + if (prioritizedGenes.length > 0 && this.settings.label.prioritizeGeneLabelsByGeneSets) { + return ( + prioritizedGenes.includes(data.gene) && + settings.rings.nonExonicFilterValues.includes(ViewModelMapper.snvClassLayer[data.mClass]) + ) + } else { + return settings.rings.nonExonicFilterValues.includes(ViewModelMapper.snvClassLayer[data.mClass]) + } + } this.snvRingFilter = (data: Data) => { if (prioritizedGenes.length > 0 && this.settings.label.prioritizeGeneLabelsByGeneSets) { @@ -98,15 +106,15 @@ export default class DataMapper { const indexA = this.reference.chromosomesOrder.indexOf(dObject.chrA) const indexB = this.reference.chromosomesOrder.indexOf(dObject.chrB) - if (dObject.dt == MutationTypes.SNV) { + if (dObject.dt == dtsnvindel) { if (index != -1 && this.snvData.length < this.settings.snv.maxMutationCount) { this.addData(dObject, dataArray) } - } else if (dObject.dt == MutationTypes.FUSION || dObject.dt == MutationTypes.SV) { + } else if (dObject.dt == dtfusionrna || dObject.dt == dtsv) { if (indexA != -1 && indexB != -1) { this.addData(dObject, dataArray) } - } else if ([MutationTypes.CNV, MutationTypes.LOH].includes(Number(dObject.dt))) { + } else if ([dtcnv, dtloh].includes(Number(dObject.dt))) { this.addData(dObject, dataArray) } else { throw Error('Unknown mutation type!') diff --git a/client/plots/disco/data/MutationTypes.ts b/client/plots/disco/data/MutationTypes.ts deleted file mode 100644 index bd00a31c5e..0000000000 --- a/client/plots/disco/data/MutationTypes.ts +++ /dev/null @@ -1,7 +0,0 @@ -export enum MutationTypes { - SNV = 1, - FUSION = 2, - CNV = 4, - SV = 5, - LOH = 10 -} diff --git a/client/plots/disco/label/LabelsMapper.ts b/client/plots/disco/label/LabelsMapper.ts index 4b49130048..3006d78618 100644 --- a/client/plots/disco/label/LabelsMapper.ts +++ b/client/plots/disco/label/LabelsMapper.ts @@ -6,10 +6,10 @@ import MLabel from './MLabel.ts' import MutationTooltip from '#plots/disco/label/MutationTooltip.ts' import Settings from '#plots/disco/Settings.ts' import FusionColorProvider from '#plots/disco/fusion/FusionColorProvider.ts' -import { MutationTypes } from '#plots/disco/data/MutationTypes.ts' import FusionTooltip from '#plots/disco/fusion/FusionTooltip.ts' import CnvTooltip from '#plots/disco/cnv/CnvTooltip.ts' import CnvColorProvider from '#plots/disco/cnv/CnvColorProvider.ts' +import { dtsnvindel, dtfusionrna } from '#shared/common' export default class LabelsMapper { private settings: Settings @@ -29,7 +29,7 @@ export default class LabelsMapper { const outerRadius = innerRadius + this.settings.rings.labelsToLinesDistance data.forEach(data => { - if (data.dt == MutationTypes.SNV) { + if (data.dt == dtsnvindel) { const startAngle = this.calculateStartAngle(data.chr, data.position) const endAngle = this.calculateEndAngle(data.chr, data.position) @@ -48,7 +48,7 @@ export default class LabelsMapper { ) } - if (data.dt == MutationTypes.FUSION) { + if (data.dt == dtfusionrna) { const color = FusionColorProvider.getColor(data.chrA, data.chrB) if (data.geneA) { const startAngleSource = this.calculateStartAngle(data.chrA, data.posA) diff --git a/client/plots/disco/viewmodel/ViewModel.ts b/client/plots/disco/viewmodel/ViewModel.ts index 595c5697b1..dd18bc9c35 100644 --- a/client/plots/disco/viewmodel/ViewModel.ts +++ b/client/plots/disco/viewmodel/ViewModel.ts @@ -18,6 +18,7 @@ export default class ViewModel { settings: Settings svnDataLength: number filteredSnvDataLength: number + snvDataLength genesetName: string constructor( @@ -27,7 +28,8 @@ export default class ViewModel { fusions: Array, filteredSnvDataLength: number, svnDataLength: number, - genesetName: string + genesetName: string, + snvDataLength: number ) { this.settings = settings this.rings = rings @@ -49,6 +51,7 @@ export default class ViewModel { this.legendHeight = this.calculateLegendHeight(legend) this.svnDataLength = svnDataLength this.filteredSnvDataLength = filteredSnvDataLength + this.snvDataLength = snvDataLength } getElements(ringType: RingType): Array { diff --git a/client/plots/disco/viewmodel/ViewModelMapper.ts b/client/plots/disco/viewmodel/ViewModelMapper.ts index 914754bbc8..1f98fa351f 100644 --- a/client/plots/disco/viewmodel/ViewModelMapper.ts +++ b/client/plots/disco/viewmodel/ViewModelMapper.ts @@ -3,23 +3,9 @@ import Reference from '#plots/disco/chromosome/Reference.ts' import DataMapper from '#plots/disco/data/DataMapper.ts' import Settings from '#plots/disco/Settings.ts' import ViewModelProvider from './ViewModelProvider.ts' +import { dtsnvindel, dtfusionrna } from '#shared/common' export class ViewModelMapper { - static dtNums = [2, 5, 4, 10, 1, 'exonic', 'non-exonic'] - - static dtAlias = { - 1: 'snv', // - 2: 'sv', //'fusionrna', - 3: 'geneexpression', - 4: 'cnv', - 5: 'sv', - 6: 'snv', //'itd', - 7: 'snv', //'del', - 8: 'snv', //'nloss', - 9: 'snv', //'closs', - 10: 'loh' - } - static snvClassLayer = { M: 'exonic', E: 'exonic', diff --git a/client/plots/disco/viewmodel/ViewModelProvider.ts b/client/plots/disco/viewmodel/ViewModelProvider.ts index 1d69183321..5b499e56da 100644 --- a/client/plots/disco/viewmodel/ViewModelProvider.ts +++ b/client/plots/disco/viewmodel/ViewModelProvider.ts @@ -16,6 +16,7 @@ import Labels from '#plots/disco/label/Labels.ts' import NonExonicSnvArcsMapper from '#plots/disco/snv/NonExonicSnvArcsMapper.ts' import LohArcMapper from '#plots/disco/loh/LohArcMapper.ts' import Rings from '#plots/disco/ring/Rings.ts' +import { dtsnvindel } from '#shared/common' export default class ViewModelProvider { private settings: Settings @@ -151,8 +152,9 @@ export default class ViewModelProvider { legend, fusions, dataHolder.filteredSnvData.length, - dataHolder.snvData.length - dataHolder.nonExonicSnvData.length, - this.genesetName + dataHolder.snvData.length, + this.genesetName, + data.filter(i => i.dt == dtsnvindel).length ) } } diff --git a/client/plots/matrix.interactivity.js b/client/plots/matrix.interactivity.js index 741454dff1..b41c7a527c 100644 --- a/client/plots/matrix.interactivity.js +++ b/client/plots/matrix.interactivity.js @@ -185,48 +185,6 @@ export function setInteractivity(self) { if (self.dom.sampleListMenu) self.dom.sampleListMenu.destroy() - if (self.state.termdbConfig.urlTemplates) { - const templates = self.state.termdbConfig.urlTemplates - // quick fix: should use templates[*].regex for a non-hardcoded condition - if (templates.sample) { - // quick fix, should have a more reliable namekey that is guaranteed to have the UUID for the URL construction - // maybe from refs.bySampleId, filled in by termdb.getSampleAlias in the backend - const name = sampleData[templates.sample.namekey] || sampleData.sample || sampleData.row.sample - if (!templates.sample.regex /*|| name has a matching pattern */) { - const menuDiv = self.dom.clickMenu.d.append('div').style('padding', '5px 10px').style('margin', '1px') - - menuDiv.append('span').html(`${s.controlLabels.Sample}: `) - const link = menuDiv - .append('a') - .attr('href', `${templates.sample.base}${name}`) - .attr('target', '_blank') - .html(`${sampleData.row._ref_.label} ${svgIcons.externalLink}`) - - link.on('click', async event => { - menuDiv.remove() - self.dom.clickMenu.d.selectAll('*').remove() - }) - } - } - - if (sampleData.tw?.term?.type == 'geneVariant' && templates.gene) { - const menuDiv = self.dom.clickMenu.d.append('div').style('padding', '5px 10px').style('margin', '1px') - - const name = self.data.refs.byTermId[sampleData.tw.$id][templates.gene.namekey] - menuDiv.append('span').html('Gene: ') - const link = menuDiv - .append('a') - .attr('href', `${templates.gene.base}${name}`) - .attr('target', '_blank') - .html(`${sampleData.tw.term.name} ${svgIcons.externalLink}`) - - link.on('click', async event => { - menuDiv.remove() - self.dom.clickMenu.d.selectAll('*').remove() - }) - } - } - if (q.singleSampleGenomeQuantification) { for (const k in q.singleSampleGenomeQuantification) { const menuDiv = self.dom.clickMenu.d @@ -276,6 +234,63 @@ export function setInteractivity(self) { self.dom.clickMenu.d.selectAll('*').remove() }) } + + const l = self.settings.matrix.controlLabels + const rows = [] + + const templates = self.state.termdbConfig.urlTemplates + if (templates?.sample) { + const name = sampleData[templates.sample.namekey] || sampleData.sample || sampleData.row.sample + rows.push( + `${l.Sample}: ${sampleData.row._ref_.label} ${svgIcons.externalLink}` + ) + } else rows.push(`${l.Sample}:${sampleData.row._ref_.label || sampleData.value.sample}`) + + if (sampleData.term.type != 'geneVariant') { + rows.push( + `${sampleData.term.name}: ${sampleData.convertedValueLabel || sampleData.label}` + ) + } else if (sampleData.tw?.term?.type == 'geneVariant' && sampleData.value) { + if (templates?.gene) { + const name = self.data.refs.byTermId[sampleData.tw.$id][templates.gene.namekey] + rows.push( + `Gene:${sampleData.tw.term.name} ${svgIcons.externalLink}` + ) + } else rows.push(`Gene:${sampleData.term.name}`) + + const siblingCellLabels = {} + for (const c of sampleData.siblingCells) { + if (c.$id != sampleData.$id) continue + const v = c.value + const p = v.pairlst + const dtLabel = v.origin ? `${v.origin} ${dt2label[v.dt]}` : dt2label[v.dt] + const label = + c.label == self.config.settings.hierCluster?.termGroupName + ? v.value + : p + ? (p[0].a.name || p[0].a.chr) + '::' + (p[0].b.name || p[0].b.chr) + : v.mname + ? `${v.mname} ${mclass[v.class].label}` + : mclass[v.class].label + const color = c.fill == v.color || v.class == 'Blank' ? '' : c.fill + + if (!siblingCellLabels[dtLabel]) { + siblingCellLabels[dtLabel] = [{ label, color }] + } else { + siblingCellLabels[dtLabel].push({ label, color }) + } + } + for (const [dtLabel, classArray] of Object.entries(siblingCellLabels).sort((a, b) => b.length - a.length)) { + rows.push(`${dtLabel}:${classArray[0].label}`) + for (const classType of classArray.slice(1)) { + rows.push(`${classType.label}`) + } + } + } + const menuDiv = self.dom.clickMenu.d.append('div').style('padding', '5px 9px') + menuDiv.append('span').html(`${rows.join('\n')}
`) self.dom.clickMenu.show(event.clientX, event.clientY, false, true) } @@ -845,7 +860,8 @@ function setTermActions(self) { async function submit_lst(termlst) { const newterms = await Promise.all( - termlst.map(async term => { + termlst.map(async _term => { + const term = structuredClone(_term) const tw = 'id' in term ? { id: term.id, term } : { term } await fillTermWrapper(tw) return tw diff --git a/client/plots/violin.js b/client/plots/violin.js index c41582d251..6462ae610e 100644 --- a/client/plots/violin.js +++ b/client/plots/violin.js @@ -37,7 +37,10 @@ class ViolinPlot { controls, violinDiv: holder .append('div') - .attr('id', 'sjpp-vp-violinDiv') + // set attr('class') class when using a constant string value below; + // should not set a constant attr('id') value for the violinDiv, + // since multiple violin plots in the same view must not share the same ID value + .attr('class', 'sjpp-vp-violinDiv') .style('padding-left', this.opts.mode != 'minimal' ? '10px' : '0px'), legendDiv: holder.append('div').classed('sjpp-vp-legend', true).style('padding-left', '5px'), diff --git a/client/src/block.tk.bam.gdc.js b/client/src/block.tk.bam.gdc.js index eaa224e465..d32c721005 100644 --- a/client/src/block.tk.bam.gdc.js +++ b/client/src/block.tk.bam.gdc.js @@ -8,7 +8,7 @@ import { addGeneSearchbox, string2variant } from '#dom/genesearch' import { Menu } from '#dom/menu' import { Tabs } from '../dom/toggleButtons' import { renderTable } from '#dom/table' -import { make_table_2col } from '#dom/table2col' +import { table2col } from '../dom/table2col' /* @@ -401,15 +401,15 @@ export async function bamsliceui({ } gdc_args.bam_files.push(file) - const rows = [] + const table = table2col({ holder: baminfo_table }) for (const row of baminfo_rows) { - rows.push({ - k: row.title, - v: row.url ? `${onebam[row.key]}` : onebam[row.key] - }) + const [td1, td2] = table.addRow() + td1.text(row.title) + td2.html( + row.url ? `${onebam[row.key]}` : onebam[row.key] + ) file.about.push({ k: row.title, v: onebam[row.key] }) } - make_table_2col(baminfo_table, rows) } function update_multifile_table(files) { diff --git a/client/src/block.tk.bam.js b/client/src/block.tk.bam.js index 71acb8e6e2..be35aaff07 100644 --- a/client/src/block.tk.bam.js +++ b/client/src/block.tk.bam.js @@ -1704,9 +1704,7 @@ function configPanel(tk, block) { callback: v => { tk.asPaired = v loadTk(tk, block) - }, - inputName: 'show-reads-radios' /* Fix for show reads and strictness radio - buttons operating independently */ + } }) } { @@ -1767,9 +1765,7 @@ function configPanel(tk, block) { callback: v => { tk.strictness = v loadTk(tk, block) - }, - inputName: 'strictness-radios' /* Fix for show reads and strictness radio - buttons operating independently */ + } }) } diff --git a/client/src/gdc.maf.js b/client/src/gdc.maf.js index c92619b371..b0850d757d 100644 --- a/client/src/gdc.maf.js +++ b/client/src/gdc.maf.js @@ -3,6 +3,7 @@ import { sayerror } from '../dom/sayerror.ts' import { renderTable } from '#dom/table' import { make_radios } from '#dom/radiobutton' import { fileSize } from '#shared/fileSize' +import { Menu } from '#dom/menu' /* a UI to list open-access maf files from current cohort @@ -14,8 +15,152 @@ filter0=str */ +const tip = new Menu() + // list of columns to show in MAF file table -const columns = [{ label: 'Case' }, { label: 'Project' }, { label: 'Samples' }, { label: 'File Size' }] +const tableColumns = [{ label: 'Case' }, { label: 'Project' }, { label: 'Samples' }, { label: 'File Size' }] +// list of gdc maf file columns; selected ones are used for output +const mafColumns = [ + { column: 'Hugo_Symbol', selected: true }, + { column: 'Entrez_Gene_Id', selected: true }, + { column: 'Center', selected: true }, + { column: 'NCBI_Build', selected: true }, + { column: 'Chromosome', selected: true }, + { column: 'Start_Position', selected: true }, + { column: 'End_Position', selected: true }, + { column: 'Strand', selected: true }, + { column: 'Variant_Classification', selected: true }, + { column: 'Variant_Type', selected: true }, + { column: 'Reference_Allele', selected: true }, + { column: 'Tumor_Seq_Allele1', selected: true }, + { column: 'Tumor_Seq_Allele2', selected: true }, + { column: 'dbSNP_RS', selected: true }, + { column: 'dbSNP_Val_Status', selected: true }, + { column: 'Tumor_Sample_Barcode', selected: true }, + { column: 'Matched_Norm_Sample_Barcode', selected: true }, + { column: 'Match_Norm_Seq_Allele1', selected: true }, + { column: 'Match_Norm_Seq_Allele2', selected: true }, + { column: 'Tumor_Validation_Allele1', selected: true }, + { column: 'Tumor_Validation_Allele2', selected: true }, + { column: 'Match_Norm_Validation_Allele1', selected: true }, + { column: 'Match_Norm_Validation_Allele2', selected: true }, + { column: 'Verification_Status', selected: true }, + { column: 'Validation_Status', selected: true }, + { column: 'Mutation_Status', selected: true }, + { column: 'Sequencing_Phase', selected: true }, + { column: 'Sequence_Source', selected: true }, + { column: 'Validation_Method', selected: true }, + { column: 'Score', selected: true }, + { column: 'BAM_File', selected: true }, + { column: 'Sequencer', selected: true }, + { column: 'Tumor_Sample_UUID', selected: true }, + { column: 'Matched_Norm_Sample_UUID', selected: true }, + { column: 'HGVSc', selected: true }, + { column: 'HGVSp', selected: true }, + { column: 'HGVSp_Short', selected: true }, + { column: 'Transcript_ID', selected: true }, + { column: 'Exon_Number', selected: true }, + { column: 't_depth', selected: true }, + { column: 't_ref_count', selected: true }, + { column: 't_alt_count', selected: true }, + { column: 'n_depth', selected: true }, + { column: 'n_ref_count', selected: true }, + { column: 'n_alt_count', selected: true }, + { column: 'all_effects', selected: true }, + { column: 'Allele', selected: true }, + { column: 'Gene', selected: true }, + { column: 'Feature', selected: true }, + { column: 'Feature_type', selected: true }, + { column: 'One_Consequence', selected: true }, + { column: 'Consequence', selected: true }, + { column: 'cDNA_position', selected: true }, + { column: 'CDS_position', selected: true }, + { column: 'Protein_position', selected: true }, + { column: 'Amino_acids', selected: true }, + { column: 'Codons', selected: true }, + { column: 'Existing_variation', selected: true }, + { column: 'DISTANCE', selected: true }, + { column: 'TRANSCRIPT_STRAND', selected: true }, + { column: 'SYMBOL', selected: true }, + { column: 'SYMBOL_SOURCE', selected: true }, + { column: 'HGNC_ID', selected: true }, + { column: 'BIOTYPE', selected: true }, + { column: 'CANONICAL', selected: true }, + { column: 'CCDS', selected: true }, + { column: 'ENSP', selected: true }, + { column: 'SWISSPROT', selected: true }, + { column: 'TREMBL', selected: true }, + { column: 'UNIPARC', selected: true }, + { column: 'UNIPROT_ISOFORM', selected: true }, + { column: 'RefSeq', selected: true }, + { column: 'MANE', selected: true }, + { column: 'APPRIS', selected: true }, + { column: 'FLAGS', selected: true }, + { column: 'SIFT', selected: true }, + { column: 'PolyPhen', selected: true }, + { column: 'EXON', selected: true }, + { column: 'INTRON', selected: true }, + { column: 'DOMAINS', selected: true }, + { column: '1000G_AF', selected: true }, + { column: '1000G_AFR_AF', selected: true }, + { column: '1000G_AMR_AF', selected: true }, + { column: '1000G_EAS_AF', selected: true }, + { column: '1000G_EUR_AF', selected: true }, + { column: '1000G_SAS_AF', selected: true }, + { column: 'ESP_AA_AF', selected: true }, + { column: 'ESP_EA_AF', selected: true }, + { column: 'gnomAD_AF', selected: true }, + { column: 'gnomAD_AFR_AF', selected: true }, + { column: 'gnomAD_AMR_AF', selected: true }, + { column: 'gnomAD_ASJ_AF', selected: true }, + { column: 'gnomAD_EAS_AF', selected: true }, + { column: 'gnomAD_FIN_AF', selected: true }, + { column: 'gnomAD_NFE_AF', selected: true }, + { column: 'gnomAD_OTH_AF', selected: true }, + { column: 'gnomAD_SAS_AF', selected: true }, + { column: 'MAX_AF', selected: true }, + { column: 'MAX_AF_POPS', selected: true }, + { column: 'gnomAD_non_cancer_AF', selected: true }, + { column: 'gnomAD_non_cancer_AFR_AF', selected: true }, + { column: 'gnomAD_non_cancer_AMI_AF', selected: true }, + { column: 'gnomAD_non_cancer_AMR_AF', selected: true }, + { column: 'gnomAD_non_cancer_ASJ_AF', selected: true }, + { column: 'gnomAD_non_cancer_EAS_AF', selected: true }, + { column: 'gnomAD_non_cancer_FIN_AF', selected: true }, + { column: 'gnomAD_non_cancer_MID_AF', selected: true }, + { column: 'gnomAD_non_cancer_NFE_AF', selected: true }, + { column: 'gnomAD_non_cancer_OTH_AF', selected: true }, + { column: 'gnomAD_non_cancer_SAS_AF', selected: true }, + { column: 'gnomAD_non_cancer_MAX_AF_adj', selected: true }, + { column: 'gnomAD_non_cancer_MAX_AF_POPS_adj', selected: true }, + { column: 'CLIN_SIG', selected: true }, + { column: 'SOMATIC', selected: true }, + { column: 'PUBMED', selected: true }, + { column: 'TRANSCRIPTION_FACTORS', selected: true }, + { column: 'MOTIF_NAME', selected: true }, + { column: 'MOTIF_POS', selected: true }, + { column: 'HIGH_INF_POS', selected: true }, + { column: 'MOTIF_SCORE_CHANGE', selected: true }, + { column: 'miRNA', selected: true }, + { column: 'IMPACT', selected: true }, + { column: 'PICK', selected: true }, + { column: 'VARIANT_CLASS', selected: true }, + { column: 'TSL', selected: true }, + { column: 'HGVS_OFFSET', selected: true }, + { column: 'PHENO', selected: true }, + { column: 'GENE_PHENO', selected: true }, + { column: 'CONTEXT', selected: true }, + { column: 'case_id', selected: true }, + { column: 'GDC_FILTER', selected: true }, + { column: 'COSMIC', selected: true }, + { column: 'hotspot', selected: true }, + { column: 'normal_bam_uuid', selected: true }, + { column: 'RNA_Support', selected: true }, + { column: 'RNA_depth', selected: true }, + { column: 'RNA_ref_count', selected: true }, + { column: 'RNA_alt_count', selected: true }, + { column: 'callers', selected: true } +] export async function gdcMAFui({ holder, filter0, callbackOnRender, debugmode = false }) { // public api obj to be returned @@ -26,18 +171,32 @@ export async function gdcMAFui({ holder, filter0, callbackOnRender, debugmode = callbackOnRender(publicApi) } - const obj = { - // old habit of wrapping everything - errDiv: holder.append('div'), - controlDiv: holder.append('div'), - tableDiv: holder.append('div'), - opts: { - filter0, - experimentalStrategy: 'WXS' + try { + { + // validate column names in case of human err + const cn = new Set() + for (const c of mafColumns) { + if (!c.column) throw '.column missing from an element' + if (cn.has(c.column)) throw 'duplicate column: ' + c.column + cn.add(c.column) + } + } + const obj = { + // old habit of wrapping everything + errDiv: holder.append('div'), + controlDiv: holder.append('div'), + tableDiv: holder.append('div'), + opts: { + filter0, + experimentalStrategy: 'WXS' + } } + makeControls(obj) + await getFilesAndShowTable(obj) + } catch (e) { + console.log(e) + sayerror(holder, e.message || e) } - makeControls(obj) - await getFilesAndShowTable(obj) return publicApi // ? } @@ -75,27 +234,54 @@ function makeControls(obj) { } }) } + { + const tr = table.append('tr') + tr.append('td').style('opacity', 0.5).text('Output Columns') + const td = tr.append('td') + const clickText = td + .append('span') + .attr('class', 'sja_clbtext') + .on('click', event => { + const rows = [], + selectedRows = [] + for (const [i, c] of mafColumns.entries()) { + rows.push([{ value: c.column }]) + if (c.selected) selectedRows.push(i) + } + renderTable({ + div: tip.clear().showunder(event.target).d, + rows, + columns: [{ label: 'Column Name' }], + selectedRows, + noButtonCallback: (i, n) => { + mafColumns[i].selected = n.checked + updateText() + } + }) + }) + + updateText() + + function updateText() { + clickText.text( + `${mafColumns.reduce((c, i) => c + (i.selected ? 1 : 0), 0)} of ${ + mafColumns.length + } columns selected, click to change` + ) + } + } } async function getFilesAndShowTable(obj) { obj.tableDiv.selectAll('*').remove() const wait = obj.tableDiv.append('div').text('Loading...') - let result - { - const body = { - experimentalStrategy: obj.opts.experimentalStrategy - } - if (obj.opts.filter0) body.filter0 = obj.opts.filter0 - try { - result = await dofetch3('gdc/maf', { body }) - if (result.error) throw result.error - } catch (e) { - wait.remove() - sayerror(obj.errDiv, e) - return - } + const body = { + experimentalStrategy: obj.opts.experimentalStrategy } + if (obj.opts.filter0) body.filter0 = obj.opts.filter0 + const result = await dofetch3('gdc/maf', { body }) + if (result.error) throw result.error wait.remove() // render @@ -130,7 +316,7 @@ async function getFilesAndShowTable(obj) { } renderTable({ rows, - columns, + columns: tableColumns, resize: true, div: obj.tableDiv.append('div'), selectAll: true, @@ -159,6 +345,12 @@ async function getFilesAndShowTable(obj) { } async function submitSelectedFiles(lst, button) { + const outColumns = mafColumns.filter(i => i.selected).map(i => i.column) + if (outColumns.length == 0) { + window.alert('No output columns selected.') + return + } + const fileIdLst = [] for (const i of lst) { fileIdLst.push(result.files[i].id) @@ -172,7 +364,7 @@ async function getFilesAndShowTable(obj) { let data try { - data = await dofetch3('gdc/mafBuild', { body: { fileIdLst } }) + data = await dofetch3('gdc/mafBuild', { body: { fileIdLst, columns: outColumns } }) if (data.error) throw data.error } catch (e) { sayerror(obj.errDiv, e) diff --git a/client/termdb/TermdbVocab.js b/client/termdb/TermdbVocab.js index 403bb19755..3d283f0f00 100644 --- a/client/termdb/TermdbVocab.js +++ b/client/termdb/TermdbVocab.js @@ -713,7 +713,6 @@ export class TermdbVocab extends Vocab { assumption is that if this array is not empty, request for a dictionary term also from opts.terms[] will only retrieve samples mutated on this gene list, rather than whole cohort if currentGeneNames[] is empty, then dict term data return will not be restricted - XXX (not working yet) this is general and not specific to gdc */ const currentGeneNames = opts.terms .filter(tw => tw.term.type === 'geneVariant') @@ -724,6 +723,8 @@ export class TermdbVocab extends Vocab { if (opts.loadingDiv) opts.loadingDiv.html('Updating data ...') // fetch the annotated sample for each term while (termsToUpdate.length) { + // request data for one term each time, empty list and break while loop + // possible to change to pop two or more each time const tw = termsToUpdate.pop() const copy = this.getTwMinCopy(tw) const init = { @@ -742,12 +743,8 @@ export class TermdbVocab extends Vocab { if (opts.filter0) init.body.filter0 = opts.filter0 // avoid adding "undefined" value if (opts.isHierCluster) init.body.isHierCluster = true // special arg from matrix, just pass along - ///////////////////////////////////////// - // !!!!!!!! FIXME !!!!!!!!!!! - // do this via some settings via this.termdbConfig, replace hardcoded logic - ///////////////////////////////////////// - if (this.vocab.dslabel == 'GDC' && tw.term.id && currentGeneNames.length) { - /* term.id is present meaning term is dictionary term + if (tw.term.id && currentGeneNames.length) { + /* term.id is present meaning term is dictionary term (FIXME if this is unreliable) and there are gene terms, add this to limit to mutated cases */ init.body.currentGeneNames = currentGeneNames @@ -854,6 +851,7 @@ export class TermdbVocab extends Vocab { /* Same as getAnnotatedSampleData, but only returns the lst[] of samples and makes a single request to the server. + XXX state requirements and eliminate code dup */ async getAnnotatedSampleDataSimple(opts, _refs = {}) { // may check against required auth credentials for the server route @@ -864,16 +862,6 @@ export class TermdbVocab extends Vocab { if (!headers) return const filter = getNormalRoot(opts.filter) - /************** quick fix - need list of gene names of current geneVariant terms, - so that a dictionary term will only retrieve samples mutated on this gene list, rather than whole cohort (e.g. gdc) - NOTE: sort the gene names by the default alphanumeric order to improve cache reuse even when terms are resorted - */ - const currentGeneNames = opts.terms - .filter(tw => tw.term.type === 'geneVariant') - .map(tw => tw.term.name) - .sort() - const twlst = [] for (const tw of opts.terms) twlst.push(this.getTwMinCopy(tw)) @@ -893,14 +881,6 @@ export class TermdbVocab extends Vocab { if (opts.filter0) init.body.filter0 = opts.filter0 // avoid adding "undefined" value if (opts.isHierCluster) init.body.isHierCluster = true // special arg from matrix, just pass along - ///////////////////////////////////////// - // !!!!!!!! FIXME !!!!!!!!!!! - // do this via some settings via this.termdbConfig, replace hardcoded logic - ///////////////////////////////////////// - if (this.vocab.dslabel == 'GDC' && tw.term.id && currentGeneNames.length) { - init.body.currentGeneNames = currentGeneNames - } - const data = await dofetch3('termdb', init) const result = [] //mapped to expected value for (const id in data.samples) result.push(data.samples[id]) diff --git a/rust/src/gdcmaf.rs b/rust/src/gdcmaf.rs index ec31ace992..aefe27932c 100644 --- a/rust/src/gdcmaf.rs +++ b/rust/src/gdcmaf.rs @@ -7,7 +7,7 @@ Output gzip compressed maf file to stdout. Example of usage: - echo '{"host": "https://api.gdc.cancer.gov/data/", "fileIdLst": ["8b31d6d1-56f7-4aa8-b026-c64bafd531e7", "b429fcc1-2b59-4b4c-a472-fb27758f6249"]}'|./target/release/gdcmaf + echo '{"host": "https://api.gdc.cancer.gov/data/","columns": ["Hugo_Symbol", "Entrez_Gene_Id", "Center", "NCBI_Build", "Chromosome", "Start_Position"], "fileIdLst": ["8b31d6d1-56f7-4aa8-b026-c64bafd531e7", "b429fcc1-2b59-4b4c-a472-fb27758f6249"]}'|./target/release/gdcmaf */ use flate2::read::GzDecoder; @@ -20,7 +20,7 @@ use std::io::{self,Read,Write}; -fn select_maf_col(d:String) -> Vec { +fn select_maf_col(d:String,columns:&Vec) -> Vec { let mut maf_str: String = String::new(); let mut header_indices: Vec = Vec::new(); let lines = d.trim_end().split("\n"); @@ -29,9 +29,12 @@ fn select_maf_col(d:String) -> Vec { continue } else if line.contains("Hugo_Symbol") { let header: Vec = line.split("\t").map(|s| s.to_string()).collect(); - for col in MAF_COL { - let col_index: usize = header.iter().position(|x| x == col).unwrap(); - header_indices.push(col_index); + for col in columns { + if let Some(index) = header.iter().position(|x| x == col) { + header_indices.push(index); + } else { + panic!("{} was not found!",col); + } } } else { let maf_cont_lst: Vec = line.split("\t").map(|s| s.to_string()).collect(); @@ -47,27 +50,6 @@ fn select_maf_col(d:String) -> Vec { } -// GDC MAF columns (96) -const MAF_COL: [&str;96] = ["Hugo_Symbol", "Entrez_Gene_Id", "Center", "NCBI_Build", "Chromosome", - "Start_Position", "End_Position", "Strand", "Variant_Classification", - "Variant_Type", "Reference_Allele", "Tumor_Seq_Allele1", "Tumor_Seq_Allele2", - "dbSNP_RS", "dbSNP_Val_Status", "Tumor_Sample_Barcode", "Matched_Norm_Sample_Barcode", - "Match_Norm_Seq_Allele1", "Match_Norm_Seq_Allele2", "Tumor_Validation_Allele1", - "Tumor_Validation_Allele2", "Match_Norm_Validation_Allele1", "Match_Norm_Validation_Allele2", - "Verification_Status", "Validation_Status", "Mutation_Status", "Sequencing_Phase", - "Sequence_Source", "Validation_Method", "Score", "BAM_File", "Sequencer", - "Tumor_Sample_UUID", "Matched_Norm_Sample_UUID", "HGVSc", "HGVSp", "HGVSp_Short", - "Transcript_ID", "Exon_Number", "t_depth", "t_ref_count", "t_alt_count", "n_depth", - "n_ref_count", "n_alt_count", "all_effects", "Allele", "Gene", "Feature", "Feature_type", - "One_Consequence", "Consequence", "cDNA_position", "CDS_position", "Protein_position", - "Amino_acids", "Codons", "Existing_variation", "DISTANCE", "TRANSCRIPT_STRAND", "SYMBOL", - "SYMBOL_SOURCE", "HGNC_ID", "BIOTYPE", "CANONICAL", "CCDS", "ENSP", "SWISSPROT", "TREMBL", - "UNIPARC", "RefSeq", "SIFT", "PolyPhen", "EXON", "INTRON", "DOMAINS", "CLIN_SIG", "SOMATIC", - "PUBMED", "MOTIF_NAME", "MOTIF_POS", "HIGH_INF_POS", "MOTIF_SCORE_CHANGE", "IMPACT", "PICK", - "VARIANT_CLASS", "TSL", "HGVS_OFFSET", "PHENO", "GENE_PHENO", "CONTEXT", "tumor_bam_uuid", - "normal_bam_uuid", "case_id", "GDC_FILTER", "COSMIC"]; - - #[tokio::main] async fn main() -> Result<(),Box> { // Accepting the piped input json from jodejs and assign to the variable @@ -83,6 +65,22 @@ async fn main() -> Result<(),Box> { url.push(Path::new(&host).join(&v.as_str().unwrap()).display().to_string()); }; + // read columns as array from input json and convert data type from Vec to Vec + let maf_col:Vec; + if let Some(maf_col_value) = file_id_lst_js.get("columns") { + //convert Vec to Vec + if let Some(maf_col_array) = maf_col_value.as_array() { + maf_col = maf_col_array + .iter() + .map(|v| v.to_string().replace("\"","")) + .collect::>(); + } else { + panic!("Columns is not an array"); + } + } else { + panic!("Columns was not selected"); + }; + //downloading maf files parallelly and merge them into single maf file let download_futures = futures::stream::iter( url.into_iter().map(|url|{ @@ -110,13 +108,13 @@ async fn main() -> Result<(),Box> { // output let mut encoder = GzEncoder::new(io::stdout(), Compression::default()); - let _ = encoder.write_all(&MAF_COL.join("\t").as_bytes().to_vec()).expect("Failed to write header"); + let _ = encoder.write_all(&maf_col.join("\t").as_bytes().to_vec()).expect("Failed to write header"); let _ = encoder.write_all(b"\n").expect("Failed to write newline"); download_futures.buffer_unordered(20).for_each(|item| { if item.starts_with("Failed") { eprintln!("{}",item); } else { - let maf_bit = select_maf_col(item); + let maf_bit = select_maf_col(item,&maf_col); let _ = encoder.write_all(&maf_bit).expect("Failed to write file"); }; async {} diff --git a/server/routes/gdc.mafBuild.ts b/server/routes/gdc.mafBuild.ts index e65958ca57..227f2f3d9d 100644 --- a/server/routes/gdc.mafBuild.ts +++ b/server/routes/gdc.mafBuild.ts @@ -43,10 +43,15 @@ async function buildMaf(q: GdcMafBuildRequest, res: any) { const t0 = Date.now() const fileLst2 = (await getFileLstUnderSizeLimit(q.fileIdLst)) as string[] - console.log('test gdc maf sizes', Date.now() - t0) + if (serverconfig.debugmode) + console.log( + `${fileLst2.length} out of ${q.fileIdLst.length} input MAF files accepted by size limit`, + Date.now() - t0 + ) const arg = { fileIdLst: fileLst2, + columns: q.columns, host: path.join(apihost, 'data') // must use the /data/ endpoint from current host } @@ -55,7 +60,7 @@ async function buildMaf(q: GdcMafBuildRequest, res: any) { res.setHeader('Content-Disposition', 'attachment; filename=cohort.maf.gz') rustStream.pipe(res) - console.log('rust gdcmaf', Date.now() - t0) + if (serverconfig.debugmode) console.log('rust gdcmaf', Date.now() - t0) rustStream.on('end', () => { res.end() diff --git a/server/routes/termdb.categories.ts b/server/routes/termdb.categories.ts index c20ebfa6fc..49dd22afa6 100644 --- a/server/routes/termdb.categories.ts +++ b/server/routes/termdb.categories.ts @@ -88,7 +88,7 @@ function init({ genomes }) { } async function trigger_getcategories( - q: { tid: string | number; type: string; filter: any; term1_q: any; currentGeneNames: any }, + q: { tid: string | number; type: string; filter: any; term1_q: any; currentGeneNames?: string[]; rglst?: any }, res: { send: (arg0: { lst: any[]; orderedLabels: any }) => void }, tdb: { q: { termjsonByOneid: (arg0: any) => any } }, ds: { assayAvailability: { byDt: { [s: string]: any } | ArrayLike } }, @@ -99,13 +99,15 @@ async function trigger_getcategories( if (!q.tid) throw '.tid missing' const term = q.type == 'geneVariant' ? { name: q.tid, type: 'geneVariant', isleaf: true } : tdb.q.termjsonByOneid(q.tid) + const arg = { filter: q.filter, terms: q.type == 'geneVariant' ? [{ term: term, q: { isAtomic: true } }] : [{ id: q.tid, term, q: q.term1_q || getDefaultQ(term, q) }], - currentGeneNames: q.currentGeneNames + currentGeneNames: q.currentGeneNames, // optional, from mds3 mayAddGetCategoryArgs() + rglst: q.rglst // optional, from mds3 mayAddGetCategoryArgs() } const data = await getData(arg, ds, genome) diff --git a/server/shared/types/routes/gdc.mafBuild.ts b/server/shared/types/routes/gdc.mafBuild.ts index 3a1da46e80..c3947e8864 100644 --- a/server/shared/types/routes/gdc.mafBuild.ts +++ b/server/shared/types/routes/gdc.mafBuild.ts @@ -6,5 +6,8 @@ export type GdcMafBuildResponse = { */ export type GdcMafBuildRequest = { + /** List of input file uuids in gdc */ fileIdLst: string[] + /** List of columns in output MAF file */ + columns: string[] } diff --git a/server/src/mds3.gdc.js b/server/src/mds3.gdc.js index 52fe81585f..a3b2a5d044 100644 --- a/server/src/mds3.gdc.js +++ b/server/src/mds3.gdc.js @@ -23,7 +23,6 @@ validate_query_snvindel_byrange validate_query_snvindel_byisoform snvindel_byisoform snvindel_addclass - decideSampleId gdcValidate_query_singleSampleMutation getSingleSampleMutations getCnvFusion4oneCase @@ -104,10 +103,27 @@ export function validate_variant2sample(a) { } export function validate_query_snvindel_byrange(ds) { - ds.queries.snvindel.byrange.get = async opts => { + /* +q{} + .rglst[] + .hiddenmclass: Set + .isoform: +*/ + ds.queries.snvindel.byrange.get = async q => { + return await ds.queries.snvindel.byisoform.get(q) + + /********* + + the graphql query is not used; byisoform.get() now works to pull ssm by rglst[] + + previous reason for using graphql instead of Rest api is assumption that intergenic ssm (not on any isoform) are not available from s/ssms and /ssm_occurrences endpoints + now that these endpoints actually support range query, will use them for now + and look into enabling genomic mode on gdc view + to confirm that whether intergenic ssm are indeed indexed on Rest; if so can delete graphql below + const response = await got.post(apihostGraphql, { - headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, // xxx - body: JSON.stringify({ query: query_range2ssm, variables: variables_range2ssm(opts) }) + headers: getheaders(q), + body: JSON.stringify({ query: query_range2ssm, variables: variables_range2ssm(q) }) }) let re try { @@ -127,28 +143,34 @@ export function validate_query_snvindel_byrange(ds) { alt: h.node.tumor_allele, samples: [] } - if (h.node.consequence && h.node.consequence.hits && h.node.consequence.hits.edges) { - m.csqcount = h.node.consequence.hits.edges.length + if(!Array.isArray(h.node?.consequence?.hits?.edges)) throw 'h.node.consequence.hits.edges[] not array' + m.csqcount = h.node.consequence.hits.edges.length + { let c // consequence - if (opts.isoform) { + if (q.isoform) { c = h.node.consequence.hits.edges.find(i => i.node.transcript.transcript_id == opts.isoform) } else { c = h.node.consequence.hits.edges.find(i => i.node.transcript.is_canonical) } - const c2 = c || h.node.consequence.hits.edges[0] - // c2: { node: {consequence} } snvindel_addclass(m, (c || h.node.consequence.hits.edges[0]).node) - } - if (h.node.occurrence.hits.edges) { - for (const c of h.node.occurrence.hits.edges) { - const sample = makeSampleObj(c.node.case, ds) - sample.sample_id = c.node.case.case_id - m.samples.push(sample) + if(q.hiddenmclass && q.hiddenmclass.has(m.class)) { + // m filtered by class + continue } } + if(!Array.isArray(h.node?.occurrence?.hits?.edges)) throw 'h.node.occurrence.hits.edges[] not array' + for (const c of h.node.occurrence.hits.edges) { + const sample = makeSampleObj(c.node.case, ds) + / currently query only returns case uuid; when the query is fixed to be able to return both aliquot and case id, + can change getter to return either based on q{} setting + / + sample.sample_id = c.node.case.case_id + m.samples.push(sample) + } mlst.push(m) } return mlst + */ } } @@ -366,9 +388,13 @@ export function validate_query_snvindel_byisoform(ds) { .isoform:str required - .useCaseid4sample:true - if true, use case id for sample_id - otherwise, use sample(aliquot) id + + .gdcUseCaseuuid:true + (gdc specific parameter) + determines what kind of value is "sample_id" property of every sample: + if true, is case uuid, to count data points by cases for getData() + else, is aliquot uuid, to count data by samples + .hiddenmclass = set .filter0 read-only gdc cohort filter, pass to gdc api as-is @@ -408,24 +434,14 @@ export function validate_query_snvindel_byisoform(ds) { m.samples = [] for (const c of ssm.cases) { - const s = { sample_id: await decideSampleId(c, ds, opts.useCaseid4sample) } - if (opts.useCaseid4sample) { - // when flag is true, this query came from getData() for gdc matrix - // sample_id is case uuid, the required unique identifier to align columns - // also create optional attribute __sampleName with submitter_id value, for displaying to user - s.__sampleName = c.submitter_id - } - - /* remain to see if okay not to pass this - if (c.case_id) { - when case_id is available, pass it on to returned data as "case.case_id" - this is used by gdc matrix case selection where case_id (uuid) is needed to build cohort in gdc portal, - but the submitter_id cannot be used as it will prevent mds3 gdc filter to work, it only works with uuid - see __matrix_case_id__ - s['case.case_id'] = c.case_id + const s = {} + if (opts.gdcUseCaseuuid) { + s.sample_id = c.case_id // case uuid + if (!s.sample_id) throw 'gdcUseCaseuuid=true but c.case_id undefined' + } else { + s.sample_id = c.observation?.[0]?.sample?.tumor_sample_uuid + if (!s.sample_id) throw 'gdcUseCaseuuid=false but c.observation?.[0]?.sample?.tumor_sample_uuid undefined' } - */ - m.samples.push(s) } mlst.push(m) @@ -774,8 +790,7 @@ export function validate_query_geneCnv2(ds) { } // each hit is one gain/loss event in one case, and is reshaped into m{ samples[] } const sample = { - sample_id: hit.case.case_id, - __sampleName: hit.case.submitter_id + sample_id: hit.case.case_id } if (opts.twLst) { @@ -1118,9 +1133,7 @@ function getheaders(q) { } async function snvindel_byisoform(opts, ds) { - if (!opts.isoform) throw 'snvindel_byisoform: .isoform missing' - if (typeof opts.isoform != 'string') throw '.isoform value not string' - + // query ssm by either isoform or rglst const query1 = isoform2ssm_query1_getvariant, query2 = isoform2ssm_query2_getcase @@ -1162,11 +1175,14 @@ async function snvindel_byisoform(opts, ds) { if (!h.consequence) throw '.consequence[] missing from a ssm' if (!Number.isInteger(h.start_position)) throw 'hit.start_position is not integer' h.csqcount = h.consequence.length - const consequence = h.consequence.find(i => i.transcript.transcript_id == opts.isoform) - if (!consequence) { - // may alert?? + + let c + if (opts.isoform) { + c = h.consequence.find(i => i.transcript.transcript_id == opts.isoform) + } else { + c = h.consequence.find(i => i.transcript.is_canonical) } - h.consequence = consequence // keep only info for this isoform + h.consequence = c || h.consequence[0] h.cases = [] id2ssm.set(h.ssm_id, h) } @@ -1449,6 +1465,7 @@ q{} .rglst[] .filterObj .filter0 + .gdcUseCaseuuid = true, see byisoform.get() twLst[] array of termwrapper objects, for sample-annotating terms (not geneVariant) @@ -1511,6 +1528,13 @@ export async function querySamples_gdcapi(q, twLst, ds, geneTwLst) { } } + /* this function can identify sample either by case uuid, or sample submitter id + for simplicity, always request these two different values + */ + if (!twLst.some(i => i.id == 'case.observation.sample.tumor_sample_uuid')) + twLst.push({ term: { id: 'case.observation.sample.tumor_sample_uuid' } }) + if (!twLst.some(i => i.id == 'case.case_id')) twLst.push({ term: { id: 'case.case_id' } }) + if (q.get == 'samples') { // getting list of samples (e.g. for table display) // need following fields for table display, add to twLst[] if missing: @@ -1531,23 +1555,7 @@ export async function querySamples_gdcapi(q, twLst, ds, geneTwLst) { twLst.push({ term: { id: 'case.observation.read_depth.t_depth' } }) if (!twLst.some(i => i.id == 'case.observation.read_depth.n_depth')) twLst.push({ term: { id: 'case.observation.read_depth.n_depth' } }) - - // get aliquot id for converting to sample name from which the mutation is detected - // TODO no need when - if (!twLst.some(i => i.id == 'case.observation.sample.tumor_sample_uuid')) - twLst.push({ term: { id: 'case.observation.sample.tumor_sample_uuid' } }) } - - // need both case_id and submitter id - // need case_id to generate url/cases/ link in table display; submitter and aliquot id won't work - if (!twLst.some(i => i.id == 'case.case_id')) twLst.push({ term: { id: 'case.case_id' } }) - // need submitter id for - if (!twLst.some(i => i.id == 'case.submitter_id')) twLst.push({ term: { id: 'case.submitter_id' } }) - } - - if (q.get == 'summary' || q.get == 'sunburst') { - // submitter id is sufficient to count unique number of samples, no need for case_id - if (!twLst.some(i => i.id == 'case.submitter_id')) twLst.push({ term: { id: 'case.submitter_id' } }) } const dictTwLst = [] @@ -1566,6 +1574,7 @@ export async function querySamples_gdcapi(q, twLst, ds, geneTwLst) { */ fields.push('ssm.consequence.transcript.consequence_type') fields.push('ssm.consequence.transcript.transcript_id') + fields.push('ssm.consequence.transcript.is_canonical') } if (q.rglst) { @@ -1623,47 +1632,37 @@ export async function querySamples_gdcapi(q, twLst, ds, geneTwLst) { } } */ + let c + if (q.isoform) { + c = s.ssm.consequence.find(i => i.transcript.transcript_id == q.isoform) + } else { + c = s.ssm.consequence.find(i => i.transcript.is_canonical) + } const m = {} - snvindel_addclass( - m, - s.ssm.consequence.find(i => i.transcript.transcript_id == q.isoform) - ) + snvindel_addclass(m, c || s.ssm.consequence[0]) if (q.hiddenmclass.has(m.class)) { // this variant is dropped continue } } - if (q.rglst) { - if (!s.ssm?.chromosome || !s.ssm?.start_position) continue // lack position, skip - if ( - !q.rglst.find( - i => i.chr == s.ssm.chromosome && i.start < s.ssm.start_position && i.stop >= s.ssm.start_position - ) - ) { - continue // out of range - } - } - - if (q.variant2samples) { - // from mds3 client - // set 3rd arg to false to not set uuid as sample_id, but to use sample submitter id - sample.sample_id = await decideSampleId(s.case, ds, false) - } else { - // from getData(), need below: - // sample_id=case uuid, __sampleName=case submitter id (not sample submitter!) + if (q.gdcUseCaseuuid) { + /* identify sample with case uuid, but no need to convert to case submitter id + as this should be for getData() query, which will fill in bySampleId{} with submitter id; + url link will be just by sample_id and no need to generate separate property for it + */ sample.sample_id = s.case.case_id - sample.__sampleName = s.case.submitter_id - } - - if (s.case.case_id) { - // for making url link on a sample - sample.sample_URLid = s.case.case_id - if (s.case?.observation?.[0]?.sample?.tumor_sample_uuid) { - // aliquot id available; append to URLid so when opening the page, the sample is auto highlighted from the tree - // per uat feedback by bill 1/6/2023 - sample.sample_URLid = sample.sample_URLid + '?bioId=' + s.case.observation[0].sample.tumor_sample_uuid - } + if (!sample.sample_id) throw 'querySamples_gdcapi: case.case_id missing' + } else { + /* identify sample as sample submitter id + sample url link is complex and must be specifically generated + */ + const aliquot_id = s.case?.observation?.[0]?.sample?.tumor_sample_uuid + if (!aliquot_id) throw 'querySamples_gdcapi: aliquot_id missing' + sample.sample_id = await ds.__gdc.aliquot2submitter.get(aliquot_id) + // append aliquot_id to case url so when opening the page, the sample is auto highlighted from the tree + // per uat feedback by bill 1/6/2023 + sample.sample_URLid = s.case.case_id + '?bioId=' + aliquot_id } for (const tw of dictTwLst) { @@ -1774,24 +1773,6 @@ async function querySamplesTwlst4hierCluster(q, twLst, ds) { return { byTermId, samples } } -/* -c is case{} -decide the value to the generic sample_id attribute -*/ -async function decideSampleId(c, ds, useCaseid4sample) { - if (useCaseid4sample && c.case_id) { - // asks for case uuid, and the id is present, then return it - return c.case_id - } - - if (c?.observation?.[0]?.sample?.tumor_sample_uuid) { - // hardcoded logic to return sample submitter id when aliquot id is present - return await ds.__gdc.aliquot2submitter.get(c.observation[0].sample.tumor_sample_uuid) - } - - return c.case_id || c.submitter_id -} - function may_add_readdepth(acase, sample) { if (!acase.observation) return // per Zhenyu, the ensemble workflow unifies the depth from all callers, can display just the first @@ -2314,7 +2295,12 @@ const isoform2ssm_query1_getvariant = { /* p={} .isoform - isoform is provided for mds3 tk loading using isoform, + optional, isoform is provided for mds3 tk loading using isoform, + .rglst[] + optional, and hardcoded to use only 1 region: + when .isoform is provided, rglst should be zoomed in on isoform and used to limit ssm + when .isoform is not provided, rglst comes from genomic query + when both .isform and .rglst are missing, case_id should be required to limit to ssm from a case .case_id case_id is provided for loading ssm from a case in gdc bam slicing ui .set_id, obsolete @@ -2326,6 +2312,13 @@ const isoform2ssm_query1_getvariant = { op: 'and', content: [] } + + let r + if (p.rglst) { + r = p.rglst[0] + validateRegion(r) + } + if (p.isoform) { if (typeof p.isoform != 'string') throw '.isoform value not string' f.content.push({ @@ -2335,8 +2328,32 @@ const isoform2ssm_query1_getvariant = { value: [p.isoform] } }) - } - if (p.case_id) { + if (r) { + // limit ssm to zoom in region + f.content.push({ + op: '>=', + content: { field: 'start_position', value: r.start } + }) + f.content.push({ + op: '<=', + content: { field: 'start_position', value: r.stop } + }) + } + } else if (r) { + // no isoform but rglst is provided + f.content.push({ + op: '=', + content: { field: 'chromosome', value: r.chr } + }) + f.content.push({ + op: '>=', + content: { field: 'start_position', value: r.start } + }) + f.content.push({ + op: '<=', + content: { field: 'start_position', value: r.stop } + }) + } else if (p.case_id) { if (typeof p.case_id != 'string') throw '.case_id value not string' f.content.push({ op: '=', @@ -2345,7 +2362,10 @@ const isoform2ssm_query1_getvariant = { value: [p.case_id] } }) + } else { + throw '.isoform, .rglst, and .case_id are all missing' } + if (p.set_id) { if (typeof p.set_id != 'string') throw '.set_id value not string' f.content.push({ @@ -2366,6 +2386,12 @@ const isoform2ssm_query1_getvariant = { } } +function validateRegion(r) { + if (typeof r != 'object') throw 'p.rglst[0] not object' + if (typeof r.chr != 'string' || !r.chr || !Number.isInteger(r.start) || !Number.isInteger(r.stop)) + throw 'p.rglst[0] not valid {chr,start,stop}' +} + // REST: get case details for each ssm, no variant-level info const isoform2ssm_query2_getcase = { endpoint: '/ssm_occurrences', @@ -2394,6 +2420,12 @@ const isoform2ssm_query2_getcase = { .filterObj */ const f = { op: 'and', content: [] } + + /* if query provides isoform, rglst[] can be used alongside isoform to restrict ssm to part of isoform (when zoomed in) + if no ssm or isoform, rglst[] must be provided (querying from genomic mode) + */ + let hasSsmOrIsoform = false + if (p.ssm_id_lst) { if (typeof p.ssm_id_lst != 'string') throw 'ssm_id_lst not string' f.content.push({ @@ -2403,6 +2435,7 @@ const isoform2ssm_query2_getcase = { value: p.ssm_id_lst.split(',') } }) + hasSsmOrIsoform = true } else if (p.isoform) { f.content.push({ op: '=', @@ -2411,31 +2444,51 @@ const isoform2ssm_query2_getcase = { value: [p.isoform] } }) + hasSsmOrIsoform = true } else if (p.isoforms) { if (!Array.isArray(p.isoforms)) throw '.isoforms[] not array' f.content.push({ op: 'in', content: { field: 'ssms.consequence.transcript.transcript_id', value: p.isoforms } }) - } else { - throw '.ssm_id_lst, .isoform, .isoforms are all missing' + hasSsmOrIsoform = true } - if (p.rglst) { - /* to filter out variants that are out of view range (e.g. zoomed in on protein) - necessary when zooming in - - !!!hardcoded to only one region!!! - - */ - f.content.push({ - op: '>=', - content: { field: 'ssms.start_position', value: p.rglst[0].start } - }) - f.content.push({ - op: '<=', - content: { field: 'ssms.start_position', value: p.rglst[0].stop } - }) + { + let r + if (p.rglst) { + // !!!hardcoded to only one region!!! + r = p.rglst[0] + validateRegion(r) + } + if (hasSsmOrIsoform) { + if (r) { + // rglst[] is optional in this case; to filter out variants that are out of view range (e.g. zoomed in on protein) necessary when zooming in + f.content.push({ + op: '>=', + content: { field: 'ssms.start_position', value: r.start } + }) + f.content.push({ + op: '<=', + content: { field: 'ssms.start_position', value: r.stop } + }) + } + } else { + // rglst is required now + if (!r) throw '.ssm_id_lst, .isoform, .isoforms, .rglst[] are all missing' + f.content.push({ + op: '=', + content: { field: 'ssms.chromosome', value: r.chr } + }) + f.content.push({ + op: '>=', + content: { field: 'ssms.start_position', value: r.start } + }) + f.content.push({ + op: '<=', + content: { field: 'ssms.start_position', value: r.stop } + }) + } } if (p.set_id) { @@ -2484,6 +2537,8 @@ TODO if can be done in protein_mutations query list of variants by genomic range (of a gene/transcript) does not include info on individual tumors the "filter" name is hardcoded and used in app.js + +FIXME change query to return aliquot uuid alongside case_id */ const query_range2ssm = `query range2variants($filters: FiltersArgument) { explore { @@ -2536,9 +2591,7 @@ const query_range2ssm = `query range2variants($filters: FiltersArgument) { } }` function variables_range2ssm(p) { - // p:{} - // .rglst[{chr/start/stop}] - // .set_id + // TODO hardcoded for only one region; may extend to handle multiple ones if (!p.rglst) throw '.rglst missing' const r = p.rglst[0] if (!r) throw '.rglst[0] missing' diff --git a/server/src/mds3.init.js b/server/src/mds3.init.js index 896f20fdb7..afebbc3e5a 100644 --- a/server/src/mds3.init.js +++ b/server/src/mds3.init.js @@ -288,13 +288,13 @@ export async function validate_termdb(ds) { ds.sampleName2Id.set(r.name, r.id) } - // XXX suggest to delete, not a good idea to dump all samples to client + // XXX delete, not a good idea to dump all samples to client ds.getSampleIdMap = samples => { - const bySampleId = {} - for (const sampleId in samples) { - bySampleId[sampleId] = ds.sampleId2Name.get(+sampleId) + const d = {} + for (const i in samples) { + d[sampleId] = ds.sampleId2Name.get(+sampleId) } - return bySampleId + return d } } @@ -2224,13 +2224,10 @@ async function getSnvindelByTerm(ds, term, genome, q) { addFormatValues: true, filter0: q.filter0, // hidden filter filterObj: q.filter, // pp filter, must change key name to "filterObj" to be consistent with mds3 client - sessionid: q.sessionid - } - - if (ds.queries.geneCnv) { - // FIXME !!!!!!!! - // may need a boolean flag to specify the geneCnv query is asking for case but not sample id, thus all queries must return data with case id - arg.useCaseid4sample = true + sessionid: q.sessionid, + // !! gdc specific parameter !! + // instructs byisoform.get() to return case uuid as sample.sample_id; more or less harmless as it's ignored by non-gdc ds + gdcUseCaseuuid: true } if (ds.queries.snvindel.byisoform) { diff --git a/server/src/mds3.variant2samples.js b/server/src/mds3.variant2samples.js index bcaa34e25a..182784267b 100644 --- a/server/src/mds3.variant2samples.js +++ b/server/src/mds3.variant2samples.js @@ -109,8 +109,6 @@ q{} actual pp filter, from mds3 client side .filter actual pp filter, request does not come from mds3 and maybe getData() -.useIntegerSampleId - if true, return integer sample id ****** mutation/genomic filters; one of below must be provided @@ -136,6 +134,14 @@ q{} client always provides this, to reflect any user changes if get=sunburst, twLst is an ordered array of terms, for which to build layered sunburst otherwise element order is not essential + +******* getter() returns +{ + samples[] + always present + byTermId{} + optional, term metadata e.g. bin labels. only gdc does it +} */ async function variant2samples_getresult(q, ds) { mayAllow2returnFormatValues(q, ds) diff --git a/server/src/termdb.barchart.js b/server/src/termdb.barchart.js index 52a8fdc239..f05c56f61f 100644 --- a/server/src/termdb.barchart.js +++ b/server/src/termdb.barchart.js @@ -387,6 +387,11 @@ function getPj(q, data, tdb, ds) { // mutates the data row, ok since // rows from db query are unique to request for (const d of terms) { + // Expect all main term data to be an array, even if single-keyed, + // in order for the input data shape to match the $key1[] template, + // and in case the row key is not already an array + if (d.key == 'key1' && !Array.isArray(row[d.key])) row[d.key] = [row[d.key]] + if (d.term.type == 'condition') { if (d.q.bar_by_grade) { if (Array.isArray(row[d.key])) { @@ -501,16 +506,18 @@ function getPj(q, data, tdb, ds) { result.rows.sort((a, b) => labels.indexOf(a) - labels.indexOf(b)) } - const labels = terms[1].orderedLabels - result.dedupCols.sort((a, b) => { - const da = `${a}`.includes('-value samples') - const db = `${b}`.includes('-value samples') - if (!da && !db) return labels.indexOf(a) - labels.indexOf(b) - if (da && db) return a < b ? -1 : 1 - if (da) return 1 - if (db) return -1 - return 0 - }) + if (result.dedupCols) { + const labels = terms[1].orderedLabels + result.dedupCols.sort((a, b) => { + const da = `${a}`.includes('-value samples') + const db = `${b}`.includes('-value samples') + if (!da && !db) return labels.indexOf(a) - labels.indexOf(b) + if (da && db) return a < b ? -1 : 1 + if (da) return 1 + if (db) return -1 + return 0 + }) + } }, sortCharts(result) { if (terms[0].orderedLabels.length) { diff --git a/server/src/termdb.matrix.js b/server/src/termdb.matrix.js index ff53c488fd..b5086cb35b 100644 --- a/server/src/termdb.matrix.js +++ b/server/src/termdb.matrix.js @@ -33,28 +33,29 @@ genome{} Returns: { - samples: {} - key: stringified integer sample id (TODO use integer) + samples{} + key: stringified integer sample id value: { sample: integerId, : {key, value}, :{ - key, label, // these two are both gene names and useless + key, label, // these two are both gene names. useless?? FIXME values:[] {gene/isoform/chr/pos/ref/alt/class/mname/dt} } } - byTermId:{} + byTermId{} + metadata about terms : bins: CTE.bins events: CTE.events these info are not available in term object and is computed during run time, and - bySampleId:{} - key: stringified integer id - value: sample name + bySampleId{} + metadata about samples (e.g. print names). avoid duplicating such in sample data elements (e.g. mutations) + [sample integer id]: {label: [string sample name for display], ...} } */ @@ -92,12 +93,18 @@ function validateArg(q, ds, genome) { async function getSampleData(q) { // dictionary and non-dictionary terms require different methods for data query const [dictTerms, nonDictTerms] = divideTerms(q.terms) - const { samples, refs } = await getSampleData_dictionaryTerms(q, dictTerms) - // sample data from all terms are added into samples data - // refs{byTermId, bySampleId} collects "meta" info on terms and samples + const [samples, byTermId] = await getSampleData_dictionaryTerms(q, dictTerms) + /* samples={} + this object collects term annotation data on all samples; even if there's no dict term it still return blank {} + non-dict term data will be appended to it + byTermId={} + collects metadata on terms + */ - // return early if all samples are filtered out by not having matching dictionary term values - if (dictTerms.length && !Object.keys(samples).length) return { samples, refs } + if (dictTerms.length && !Object.keys(samples).length) { + // return early if all samples are filtered out by not having matching dictionary term values + return { samples, refs: { byTermId, bySampleId: {} } } + } const sampleFilterSet = await mayGetSampleFilterSet(q, nonDictTerms) // conditionally returns a set of sample ids @@ -105,7 +112,9 @@ async function getSampleData(q) { // for each non dictionary term type // query sample data with its own method and append results to "samples" if (tw.term.type == 'geneVariant') { - if (q.ds.cohort?.termdb?.getGeneAlias) refs.byTermId[tw.term.name] = q.ds.cohort?.termdb?.getGeneAlias(q, tw) + if (q.ds.cohort?.termdb?.getGeneAlias) { + byTermId[tw.term.name] = q.ds.cohort?.termdb?.getGeneAlias(q, tw) + } const data = await q.ds.mayGetGeneVariantData(tw, q) @@ -152,15 +161,16 @@ async function getSampleData(q) { - future data sources need to be handled here - subject to change! */ + const bySampleId = {} for (const sid in samples) { if (q.ds.cohort?.termdb?.q?.id2sampleName) { - refs.bySampleId[sid] = { label: q.ds.cohort.termdb.q.id2sampleName(Number(sid)) } + bySampleId[sid] = { label: q.ds.cohort.termdb.q.id2sampleName(Number(sid)) } } else if (q.ds.__gdc?.caseid2submitter) { - refs.bySampleId[sid] = { label: q.ds.__gdc.caseid2submitter.get(sid) } + bySampleId[sid] = { label: q.ds.__gdc.caseid2submitter.get(sid) } } } - return { samples, refs } + return { samples, refs: { byTermId, bySampleId } } } async function mayGetSampleFilterSet(q, nonDictTerms) { @@ -221,11 +231,13 @@ output: } */ async function getSampleData_dictionaryTerms(q, termWrappers) { - if (!termWrappers.length) return { samples: {}, refs: { byTermId: {}, bySampleId: {} } } + if (!termWrappers.length) return [{}, {}] if (q.ds?.cohort?.db) { // dataset uses server-side sqlite db, must use this method for dictionary terms return await getSampleData_dictionaryTerms_termdb(q, termWrappers) } + /* gdc ds has no cohort.db. thus call v2s.get() to return sample annotations for its dictionary terms + */ if (q.ds?.variant2samples?.get) { // ds is not using sqlite db but has v2s method return await getSampleData_dictionaryTerms_v2s(q, termWrappers) @@ -234,8 +246,8 @@ async function getSampleData_dictionaryTerms(q, termWrappers) { } export async function getSampleData_dictionaryTerms_termdb(q, termWrappers) { - const samples = {} - const refs = { byTermId: {}, bySampleId: {} } + const samples = {} // to return + const byTermId = {} // to return const twByTermId = {} const filter = await getFilterCTEs(q.filter, q.ds) @@ -247,15 +259,15 @@ export async function getSampleData_dictionaryTerms_termdb(q, termWrappers) { termWrappers.map(async (tw, i) => { const CTE = await get_term_cte(q, values, i, filter, tw) if (CTE.bins) { - refs.byTermId[tw.term.id] = { bins: CTE.bins } + byTermId[tw.term.id] = { bins: CTE.bins } } if (CTE.events) { - refs.byTermId[tw.term.id] = { events: CTE.events } + byTermId[tw.term.id] = { events: CTE.events } } if (tw.term.values) { const values = Object.values(tw.term.values) if (values.find(v => 'order' in v)) { - refs.byTermId[tw.term.id] = { + byTermId[tw.term.id] = { keyOrder: values.sort((a, b) => a.order - b.order).map(v => v.key) } } @@ -304,17 +316,18 @@ export async function getSampleData_dictionaryTerms_termdb(q, termWrappers) { } } - return { samples, refs } + return [samples, byTermId] } +// FIXME this never runs async function mayQueryMutatedSamples(q) { if (!q.currentGeneNames) return // no genes, do not query mutated samples and do not limit // has genes. query samples mutated on any of these genes, collect sample id into a set and return const sampleSet = new Set() for (const geneName of q.currentGeneNames) { const tw = { term: { name: geneName, type: 'geneVariant' } } - const bySampleId = await q.ds.mayGetGeneVariantData(tw, q) - for (const [sampleId, value] of bySampleId.entries()) { + const data = await q.ds.mayGetGeneVariantData(tw, q) + for (const sampleId of data.keys()) { sampleSet.add(sampleId) } } @@ -322,8 +335,9 @@ async function mayQueryMutatedSamples(q) { } /* -using mds3 dataset, that's without server-side sqlite db -only gdc runs it +using mds3 dataset, that's without server-side sqlite db and will not execute any sql query +so far it's only gdc +later can be other api-based datasets */ async function getSampleData_dictionaryTerms_v2s(q, termWrappers) { const q2 = { @@ -332,34 +346,41 @@ async function getSampleData_dictionaryTerms_v2s(q, termWrappers) { genome: q.genome, get: 'samples', twLst: termWrappers, - useIntegerSampleId: true, // ask v2s.get() to return integer sample id - isHierCluster: q.isHierCluster // optional flag required for gdc dataset + // !! gdc specific parameter !! + // instructs querySamples_gdcapi() to return case uuid as sample.sample_id; more or less harmless as it's ignored by non-gdc ds + gdcUseCaseuuid: true, + // !! gdc specific parameter !! + isHierCluster: q.isHierCluster + } + if (q.rglst) { + // !! gdc specific parameter !! present for block tk in genomic mode + q2.rglst = q.rglst } if (q.currentGeneNames) { q2.geneTwLst = [] for (const n of q.currentGeneNames) { q2.geneTwLst.push({ term: { name: n, type: 'geneVariant' } }) } + } else { + /* do not throw here + gene list is not required for loading dict term for gdc gene exp clustering + but it's required for gdc oncomatrix and will break FIXME + */ } const data = await q.ds.variant2samples.get(q2) + /* data={samples[], byTermId{}} + data.samples[] is converted to samples{} + data.byTermId{} is returned without change + */ - const samples = {} - const refs = { byTermId: data.byTermId, bySampleId: {} } + const samples = {} // data.samples[] converts into this for (const s of data.samples) { const s2 = { sample: s.sample_id } - /* optional attribute returned by gdc dataset - s.sample_id: case uuid for aligning matrix columns - s.__sampleName: case submitter id for display - - non-gdc won't return this and will display s.sample_id - */ - if (s.__sampleName) s2.sampleName = s.__sampleName - for (const tw of termWrappers) { const v = s[tw.term.id] //////////////////////////// @@ -384,7 +405,7 @@ async function getSampleData_dictionaryTerms_v2s(q, termWrappers) { } samples[s.sample_id] = s2 } - return { samples, refs } + return [samples, data.byTermId || {}] } /*