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(``)
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 || {}]
}
/*