diff --git a/client/mass/charts.js b/client/mass/charts.js index 4c685fcafa..69f0b254e7 100644 --- a/client/mass/charts.js +++ b/client/mass/charts.js @@ -290,41 +290,7 @@ function getChartTypeList(self, state) { //use the app name defined in dataset file label: state.termdbConfig.numericDictTermCluster?.appName || 'Numeric Dictionary Term cluster', chartType: 'numericDictTermCluster', - clickTo: self.showTree_selectlst, - usecase: { - target: 'numericDictTermCluster', - detail: { exclude: state.termdbConfig.numericDictTermCluster?.exclude } - }, - updateActionBySelectedTerms: (action, termlst) => { - const twlst = termlst.map(term => ({ - term: structuredClone(term), - q: { mode: NumericModes.continuous } - })) - if (twlst.length == 1) { - // violin - action.config.chartType = 'summary' - action.config.term = twlst[0] - return - } - if (twlst.length == 2) { - // scatter - action.config.chartType = 'summary' - action.config.term = twlst[0] - action.config.term2 = twlst[1] - return - } - // 3 or more terms, launch clustering - action.config.chartType = 'hierCluster' - action.config.dataType = 'numericDictTerm' - action.config.termgroups = [ - { - name: - state.termdbConfig.numericDictTermCluster?.settings?.termGroupName || 'Numercic Dictionary Term Cluster', - lst: twlst, - type: 'hierCluster' - } - ] - } + clickTo: self.loadChartSpecificMenu } ] diff --git a/client/plots/numericDictTermCluster.js b/client/plots/numericDictTermCluster.js new file mode 100644 index 0000000000..63cce0f117 --- /dev/null +++ b/client/plots/numericDictTermCluster.js @@ -0,0 +1,76 @@ +import { NumericModes } from '#shared/terms.js' + +export function makeChartBtnMenu(holder, chartsInstance) { + /* + holder: the holder in the tooltip + chartsInstance: MassCharts instance + termdbConfig is accessible at chartsInstance.state.termdbConfig{} + mass option is accessible at chartsInstance.app.opts{} + */ + chartsInstance.dom.tip.clear() + const menuDiv = holder.append('div') + if (chartsInstance.state.termdbConfig.numericDictTermCluster?.plots) { + for (const plot of chartsInstance.state.termdbConfig.numericDictTermCluster.plots) { + /* plot: + { + name=str + } + */ + menuDiv + .append('div') + .attr('class', 'sja_menuoption sja_sharp_border') + .text(plot.name) + .on('click', async () => { + chartsInstance.dom.tip.hide() + const config = await chartsInstance.app.vocabApi.getNumericDictTermClusterByName(plot.name) + chartsInstance.app.dispatch({ + type: 'plot_create', + config + }) + }) + } + } + + const chart = { + //use the app name defined in dataset file + label: chartsInstance.state.termdbConfig.numericDictTermCluster?.appName || 'Numeric Dictionary Term cluster', + chartType: 'numericDictTermCluster', + clickTo: self.showTree_selectlst, + usecase: { + target: 'numericDictTermCluster', + detail: { exclude: chartsInstance.state.termdbConfig.numericDictTermCluster?.exclude } + }, + updateActionBySelectedTerms: (action, termlst) => { + const twlst = termlst.map(term => ({ + term: structuredClone(term), + q: { mode: NumericModes.continuous } + })) + if (twlst.length == 1) { + // violin + action.config.chartType = 'summary' + action.config.term = twlst[0] + return + } + if (twlst.length == 2) { + // scatter + action.config.chartType = 'summary' + action.config.term = twlst[0] + action.config.term2 = twlst[1] + return + } + // 3 or more terms, launch clustering + action.config.chartType = 'hierCluster' + action.config.dataType = 'numericDictTerm' + action.config.termgroups = [ + { + name: + chartsInstance.state.termdbConfig.numericDictTermCluster?.settings?.termGroupName || + 'Numercic Dictionary Term Cluster', + lst: twlst, + type: 'hierCluster' + } + ] + } + } + chartsInstance.showTree_selectlst(chart) +} diff --git a/client/termdb/TermdbVocab.js b/client/termdb/TermdbVocab.js index 41c01ef6e3..4e559a3d7b 100644 --- a/client/termdb/TermdbVocab.js +++ b/client/termdb/TermdbVocab.js @@ -1157,6 +1157,18 @@ export class TermdbVocab extends Vocab { }) } + async getNumericDictTermClusterByName(name) { + // find a pre-built numericDictTermCluster by name from this dataset + return await dofetch3('termdb', { + body: { + for: 'numericDictTermCluster', + getPlotDataByName: name, + genome: this.state.vocab.genome, + dslabel: this.state.vocab.dslabel + } + }) + } + // following two methods are hardcoded at /gdc/*. TODO change to generic method to work for all datasets async getTopMutatedGenes(arg) { return await dofetch3('gdc/topMutatedGenes', { method: 'GET', body: arg }) diff --git a/release.txt b/release.txt index 8b13789179..14fbb76024 100644 --- a/release.txt +++ b/release.txt @@ -1 +1,2 @@ - +Features: +- numericDictTermCluster: allows adding pre-built numericDictTermCluster plots \ No newline at end of file diff --git a/server/src/mds3.init.js b/server/src/mds3.init.js index 1b56cd674a..86802a27c2 100644 --- a/server/src/mds3.init.js +++ b/server/src/mds3.init.js @@ -29,7 +29,7 @@ import { get_samples, get_active_groupset } from './termdb.sql.js' import { server_init_db_queries } from './termdb.server.init.ts' import { barchart_data } from './termdb.barchart.js' import { mayInitiateScatterplots } from './termdb.scatter.js' -import { mayInitiateMatrixplots } from './termdb.matrix.js' +import { mayInitiateMatrixplots, mayInitiateNumericDictionaryTermplots } from './termdb.matrix.js' import { add_bcf_variant_filter } from './termdb.snp.js' import { validate_query_NIdata } from '#routes/brainImagingSamples.ts' import { validate_query_singleCell } from '#routes/termdb.singlecellSamples.ts' @@ -301,6 +301,8 @@ export async function validate_termdb(ds) { await mayInitiateMatrixplots(ds) + await mayInitiateNumericDictionaryTermplots(ds) + if ('minTimeSinceDx' in tdb) { if (!Number.isFinite(tdb.minTimeSinceDx)) throw 'termdb.minTimeSinceDx not number' if (tdb.minTimeSinceDx <= 0) throw 'termdb.minTimeSinceDx<=0' diff --git a/server/src/termdb.js b/server/src/termdb.js index bf51c6a93c..7555588166 100644 --- a/server/src/termdb.js +++ b/server/src/termdb.js @@ -67,6 +67,7 @@ export function handle_request_closure(genomes) { if (q.for == 'mds3queryDetails') return get_mds3queryDetails(res, ds) if (q.for == 'termTypes') return res.send(await ds.getTermTypes(q)) if (q.for == 'matrix') return await get_matrix(q, req, res, ds, genome) + if (q.for == 'numericDictTermCluster') return await get_numericDictTermCluster(q, req, res, ds, genome) if (q.for == 'getSamplesPerFilter') return await getSamplesPerFilter(q, ds, res) if (q.for == 'mds3variantData') return await get_mds3variantData(q, res, ds, genome) if (q.for == 'validateToken') { @@ -328,6 +329,18 @@ async function get_matrix(q, req, res, ds, genome) { res.send(data) } +async function get_numericDictTermCluster(q, req, res, ds, genome) { + if (q.getPlotDataByName) { + // send back the config for premade numericDictTermCluster plot + if (!ds.cohort?.termdb?.numericDictTermCluster?.plots) + throw 'ds.cohort.termdb.numericDictTermCluster.plots missing for the dataset' + const plot = ds.cohort.termdb.numericDictTermCluster.plots.find(p => p.name === q.getPlotDataByName) + if (!plot) throw 'invalid name of premade numericDictTermCluster plot' // invalid name could be attack string, avoid returning it so it won't be printed in html + res.send(plot.numericDictTermClusterConfig) + return + } +} + async function get_ProfileFacilities(q, req, res, ds, tdb) { const canDisplay = authApi.canDisplaySampleIds(req, ds) let result = [] diff --git a/server/src/termdb.matrix.js b/server/src/termdb.matrix.js index 530966b9d3..af355e354b 100644 --- a/server/src/termdb.matrix.js +++ b/server/src/termdb.matrix.js @@ -644,6 +644,26 @@ export async function mayInitiateMatrixplots(ds) { } } +/* +works with "canned" NumericDictionaryTerm plot in a dataset, e.g. data from a text file +called in mds3.init +*/ +export async function mayInitiateNumericDictionaryTermplots(ds) { + if (!ds.cohort.termdb?.numericDictTermCluster?.plots) return + if (!Array.isArray(ds.cohort.termdb.numericDictTermCluster.plots)) + throw 'cohort.termdb.numericDictTermCluster.plots is not array' + for (const p of ds.cohort.termdb.numericDictTermCluster.plots) { + if (!p.name) throw '.name missing from one of numericDictTermCluster.plots[]' + if (p.file) { + const numericDictTermClusterConfig = await read_file(path.join(serverconfig.tpmasterdir, p.file)) + p.numericDictTermClusterConfig = JSON.parse(numericDictTermClusterConfig) + if (p.getConfig) p.numericDictTermClusterConfig = p.getConfig(p.numericDictTermClusterConfig) + } else { + throw 'unknown data source of one of numericDictTermClusterConfig.plots[]' + } + } +} + async function findListOfBins(q, tw, ds) { // for non-dict terms which may lack tw.term.bins if (tw.q.type == 'custom-bin') { diff --git a/shared/types/src/dataset.ts b/shared/types/src/dataset.ts index 066aaa2125..32e4ff053a 100644 --- a/shared/types/src/dataset.ts +++ b/shared/types/src/dataset.ts @@ -1012,6 +1012,8 @@ type NumericDictTermCluster = { settings?: NumericDictTermClusterSettings /** list of numeric term ids that will be excluded from the numeric dictionary term cluster, add to usecase.detail to exclude terms*/ exclude?: string[] + /** list of pre-built numericDictTermcluster plots */ + plots?: NumericDictTermClusterPlotsEntry[] } type Survival = { @@ -1028,6 +1030,16 @@ type MatrixPlotsEntry = { getConfig?: (f: any) => void } +type NumericDictTermClusterPlotsEntry = { + name: string + file: string + settings?: { + [key: string]: any + } + /** helper function to get plot config from saved session file */ + getConfig?: (f: any) => void +} + type MatrixPlots = { plots: MatrixPlotsEntry[] }