diff --git a/end-to-end-test/local/screenshots/reference/selects_treatment_in_treatment_select_box_when_icon_present_element_chrome_1600x1000.png b/end-to-end-test/local/screenshots/reference/selects_treatment_in_treatment_select_box_when_icon_present_element_chrome_1600x1000.png new file mode 100644 index 00000000000..d6b19afa247 Binary files /dev/null and b/end-to-end-test/local/screenshots/reference/selects_treatment_in_treatment_select_box_when_icon_present_element_chrome_1600x1000.png differ diff --git a/end-to-end-test/local/screenshots/reference/shows_the_home_page_element_chrome_1600x1000.png b/end-to-end-test/local/screenshots/reference/shows_the_home_page_element_chrome_1600x1000.png index abefebcc509..e1ac11cdaa0 100644 Binary files a/end-to-end-test/local/screenshots/reference/shows_the_home_page_element_chrome_1600x1000.png and b/end-to-end-test/local/screenshots/reference/shows_the_home_page_element_chrome_1600x1000.png differ diff --git a/end-to-end-test/local/screenshots/reference/shows_treatment_profile_heatmap_track_for_treatment_element_chrome_1600x1000.png b/end-to-end-test/local/screenshots/reference/shows_treatment_profile_heatmap_track_for_treatment_element_chrome_1600x1000.png new file mode 100644 index 00000000000..9d44d213074 Binary files /dev/null and b/end-to-end-test/local/screenshots/reference/shows_treatment_profile_heatmap_track_for_treatment_element_chrome_1600x1000.png differ diff --git a/end-to-end-test/local/specs/treatment.screenshot.spec.js b/end-to-end-test/local/specs/treatment.screenshot.spec.js new file mode 100644 index 00000000000..2a2f6b02963 --- /dev/null +++ b/end-to-end-test/local/specs/treatment.screenshot.spec.js @@ -0,0 +1,42 @@ +var assert = require('assert'); +var goToUrlAndSetLocalStorage = require('../../shared/specUtils').goToUrlAndSetLocalStorage; +var assertScreenShotMatch = require('../../shared/lib/testUtils').assertScreenShotMatch; +var waitForOncoprint = require('../../shared/specUtils').waitForOncoprint; +var selectReactSelectOption = require('../../shared/specUtils').selectReactSelectOption; +var oncoprintTabUrl = require('./treatment.spec').oncoprintTabUrl; +var openHeatmapMenu = require('./treatment.spec').openHeatmapMenu; + +describe('treatment feature', () => { + + describe('oncoprint tab', () => { + + beforeEach(()=>{ + goToUrlAndSetLocalStorage(oncoprintTabUrl); + waitForOncoprint(); + }); + + it('shows treatment profile heatmap track for treatment', () => { + openHeatmapMenu(); + selectReactSelectOption( $('.oncoprint__controls__heatmap_menu'), 'IC50 values of compounds on cellular phenotype readout'); + $('.oncoprint__controls__heatmap_menu textarea').setValue('17-AAG'); + $('div.icon-area div.icon').waitForExist(); + $('button=Add Treatments to Heatmap').click(); + openHeatmapMenu(); + waitForOncoprint(); + var res = browser.checkElement('[id=oncoprintDiv]'); + assertScreenShotMatch(res); + }); + + it('selects treatment in treatment select box when icon present', () => { + openHeatmapMenu(); + selectReactSelectOption( $('.oncoprint__controls__heatmap_menu'), 'IC50 values of compounds on cellular phenotype readout'); + $('.oncoprint__controls__heatmap_menu textarea').setValue('17-AAG'); + $('div.icon-area div.icon').waitForExist(); + $('.treatment-selector .Select-control').click(); + var res = browser.checkElement('.Select-option*=17-AAG'); + assertScreenShotMatch(res); + }); + + }); + +}); diff --git a/end-to-end-test/local/specs/treatment.spec.js b/end-to-end-test/local/specs/treatment.spec.js new file mode 100644 index 00000000000..8b7f94db76c --- /dev/null +++ b/end-to-end-test/local/specs/treatment.spec.js @@ -0,0 +1,134 @@ +var assert = require('assert'); +var goToUrlAndSetLocalStorage = require('../../shared/specUtils').goToUrlAndSetLocalStorage; +var waitForOncoprint = require('../../shared/specUtils').waitForOncoprint; +var reactSelectOption = require('../../shared/specUtils').reactSelectOption; +var getReactSelectOptions = require('../../shared/specUtils').getReactSelectOptions; +var selectReactSelectOption = require('../../shared/specUtils').selectReactSelectOption; +var useExternalFrontend = require('../../shared/specUtils').useExternalFrontend; + +const CBIOPORTAL_URL = process.env.CBIOPORTAL_URL.replace(/\/$/, ""); +const oncoprintTabUrl = CBIOPORTAL_URL+'/results/oncoprint?Action=Submit&RPPA_SCORE_THRESHOLD=2.0&Z_SCORE_THRESHOLD=2.0&cancer_study_list=study_es_0&case_set_id=study_es_0_all&clinicallist=NUM_SAMPLES_PER_PATIENT%2CPROFILED_IN_study_es_0_mutations%2CPROFILED_IN_study_es_0_gistic&data_priority=0&gene_list=CDKN2A%2520MDM2%2520MDM4%2520TP53&geneset_list=%20&genetic_profile_ids_PROFILE_COPY_NUMBER_ALTERATION=study_es_0_gistic&genetic_profile_ids_PROFILE_MUTATION_EXTENDED=study_es_0_mutations&show_samples=false&tab_index=tab_visualize' + +describe('treatment feature', function() { + + this.retries(2); + + if (useExternalFrontend) { + + describe('oncoprint tab', () => { + + beforeEach(()=>{ + goToUrlAndSetLocalStorage(oncoprintTabUrl); + waitForOncoprint(); + }); + + it('shows treatment data type option in heatmap menu', () => { + openHeatmapMenu(); + assert( reactSelectOption( $('.oncoprint__controls__heatmap_menu'), 'EC50 values of compounds on cellular phenotype readout') ); + assert( reactSelectOption( $('.oncoprint__controls__heatmap_menu'), 'IC50 values of compounds on cellular phenotype readout') ); + }); + + it('shows treatment text area box in heatmap menu when treatment data type is selected', () => { + openHeatmapMenu(); + selectReactSelectOption( $('.oncoprint__controls__heatmap_menu'), 'IC50 values of compounds on cellular phenotype readout'); + assert( $('.oncoprint__controls__heatmap_menu.text-icon-area') ); + }); + + it('does not show genes of gene text area in treatment text area,and vice versa', () => { + openHeatmapMenu(); + var geneText = $('.oncoprint__controls__heatmap_menu textarea').getText(); + selectReactSelectOption( $('.oncoprint__controls__heatmap_menu'), 'IC50 values of compounds on cellular phenotype readout'); + var treatmentText = $('.oncoprint__controls__heatmap_menu textarea').getText(); + assert.notEqual(geneText, treatmentText); + selectReactSelectOption( $('.oncoprint__controls__heatmap_menu'), 'mRNA expression (microarray) Z-Score normalized'); + assert.equal($('.oncoprint__controls__heatmap_menu textarea').getText(), geneText); + }); + + it('shows treatment selection box in heatmap menu when treatment data type is selected', () => { + openHeatmapMenu(); + selectReactSelectOption( $('.oncoprint__controls__heatmap_menu'), 'IC50 values of compounds on cellular phenotype readout'); + assert( $('.oncoprint__controls__heatmap_menu.treatment-selector') ); + }); + + it('adds icon when entering a valid treatment in treatment text area', () => { + openHeatmapMenu(); + selectReactSelectOption( $('.oncoprint__controls__heatmap_menu'), 'IC50 values of compounds on cellular phenotype readout'); + $('.oncoprint__controls__heatmap_menu textarea').setValue('17-AAG'); + $('div.icon*=17-AAG').waitForExist(); + assert( $('div.icon*=17-AAG') ); + }); + + it('click of icon remove button removes icon', () => { + openHeatmapMenu(); + selectReactSelectOption( $('.oncoprint__controls__heatmap_menu'), 'IC50 values of compounds on cellular phenotype readout'); + $('.oncoprint__controls__heatmap_menu textarea').setValue('17-AAG'); + $('div.icon-area div.icon').waitForExist(); + var iconButton = $('div.icon-area div.icon-button'); + iconButton.click(); + $('div.icon-area div.icon').waitForExist(undefined, true); + assert( ! $('div.icon-area div.icon').isExisting() ); + }); + + it('removes valid treatment from treatment text area when recognized', () => { + openHeatmapMenu(); + selectReactSelectOption( $('.oncoprint__controls__heatmap_menu'), 'IC50 values of compounds on cellular phenotype readout'); + $('.oncoprint__controls__heatmap_menu textarea').setValue('17-AAG'); + $('div.icon-area div.icon').waitForExist(); + assert( ! $('.oncoprint__controls__heatmap_menu textarea').getValue() ); + }); + + it('shows all treatments in the treatment select box', () => { + openHeatmapMenu(); + selectReactSelectOption( $('.oncoprint__controls__heatmap_menu'), 'IC50 values of compounds on cellular phenotype readout'); + var treatments = getReactSelectOptions( $('.oncoprint__controls__heatmap_menu .treatment-selector') ); + assert.equal(treatments.length, 10); + }); + + it('adds treatment to icons when selected in treatment select box', () => { + openHeatmapMenu(); + selectReactSelectOption( $('.oncoprint__controls__heatmap_menu'), 'IC50 values of compounds on cellular phenotype readout'); + var treatments = getReactSelectOptions( $('.oncoprint__controls__heatmap_menu .treatment-selector') ); + var treatment = treatments[0]; + var treatmentName = treatment.getText(); + treatmentName = treatmentName.replace(/.*\((.*)\).*/, "$1") + treatment.click(); + $('div.icon*='+treatmentName).waitForExist(); + assert( $('div.icon*='+treatmentName) ); + }); + + it('filters treatment select options when using search of treatment select box', () => { + openHeatmapMenu(); + selectReactSelectOption( $('.oncoprint__controls__heatmap_menu'), 'IC50 values of compounds on cellular phenotype readout'); + var searchBox = $('.oncoprint__controls__heatmap_menu .treatment-selector .Select-control input'); + searchBox.setValue('17-AAG'); + var treatments = getReactSelectOptions( $('.oncoprint__controls__heatmap_menu .treatment-selector') ); + assert(treatments.length, 1); + }); + + it('sets `treatment_list` URL parameter', () => { + openHeatmapMenu(); + selectReactSelectOption( $('.oncoprint__controls__heatmap_menu'), 'IC50 values of compounds on cellular phenotype readout'); + $('.oncoprint__controls__heatmap_menu textarea').setValue('17-AAG'); + $('div.icon-area div.icon').waitForExist(); + $('button=Add Treatments to Heatmap').click(); + waitForOncoprint(); + var url = browser.url().value; + var regex = /treatment_list=17-AAG/; + assert(url.match(regex)); + }); + + }); + + } + +}); + +var openHeatmapMenu = () => { + var heatmapButton = browser.$('button[id=heatmapDropdown]'); + heatmapButton.click(); +} + +module.exports = { + oncoprintTabUrl: oncoprintTabUrl, + openHeatmapMenu: openHeatmapMenu, +}; \ No newline at end of file diff --git a/src/pages/resultsView/ResultsViewPageStore.ts b/src/pages/resultsView/ResultsViewPageStore.ts index f2766cd8e96..34f98a0bc8e 100644 --- a/src/pages/resultsView/ResultsViewPageStore.ts +++ b/src/pages/resultsView/ResultsViewPageStore.ts @@ -80,8 +80,10 @@ import {MergedGeneQuery} from '../../shared/lib/oql/oql-parser'; import GeneMolecularDataCache from "../../shared/cache/GeneMolecularDataCache"; import GenesetMolecularDataCache from "../../shared/cache/GenesetMolecularDataCache"; import GenesetCorrelatedGeneCache from "../../shared/cache/GenesetCorrelatedGeneCache"; +import TreatmentMolecularDataCache from "../../shared/cache/TreatmentMolecularDataCache"; import GeneCache from "../../shared/cache/GeneCache"; import GenesetCache from "../../shared/cache/GenesetCache"; +import TreatmentCache from "../../shared/cache/TreatmentCache"; import {IOncoKbData} from "../../shared/model/OncoKB"; import {generateQueryVariantId} from "../../public-lib/lib/OncoKbUtils"; import { @@ -90,7 +92,9 @@ import { ExpressionEnrichment, Geneset, GenesetDataFilterCriteria, - GenesetMolecularData + GenesetMolecularData, + Treatment, + TreatmentFilter } from "../../shared/api/generated/CBioPortalAPIInternal"; import internalClient from "../../shared/api/cbioportalInternalClientInstance"; import {CancerGene, IndicatorQueryResp} from "../../shared/api/generated/OncoKbAPI"; @@ -175,7 +179,8 @@ export const AlterationTypeConstants = { PROTEIN_LEVEL: 'PROTEIN_LEVEL', FUSION: 'FUSION', GENESET_SCORE: 'GENESET_SCORE', - METHYLATION: 'METHYLATION' + METHYLATION: 'METHYLATION', + GENERIC_ASSAY: 'GENERIC_ASSAY' }; export const AlterationTypeDisplayConstants = { @@ -884,7 +889,8 @@ export class ResultsViewPageStore { this.molecularProfilesInStudies, this.studyToDataQueryFilter, this.genes, - this.genesets + this.genesets, + this.treatmentsInStudies ], invoke:async()=>{ const ret:MolecularProfile[] = []; @@ -918,7 +924,9 @@ export class ResultsViewPageStore { ret.push(profile); } })); - } else if (profile.molecularAlterationType === AlterationTypeConstants.GENESET_SCORE) { + } else if (profile.molecularAlterationType === AlterationTypeConstants.GENESET_SCORE + || profile.molecularAlterationType === AlterationTypeConstants.GENERIC_ASSAY + ) { // geneset profile, we dont have the META projection for geneset data, so just add it /*promises.push(internalClient.fetchGeneticDataItemsUsingPOST({ geneticProfileId: molecularProfileId, @@ -2079,6 +2087,7 @@ export class ResultsViewPageStore { const MRNA_EXPRESSION = AlterationTypeConstants.MRNA_EXPRESSION; const PROTEIN_LEVEL = AlterationTypeConstants.PROTEIN_LEVEL; const METHYLATION = AlterationTypeConstants.METHYLATION; + const GENERIC_ASSAY = AlterationTypeConstants.GENERIC_ASSAY; const selectedMolecularProfileIds = stringListToSet( this.selectedMolecularProfiles.result!.map((profile)=>profile.molecularProfileId) ); @@ -2086,12 +2095,13 @@ export class ResultsViewPageStore { const expressionHeatmaps = _.sortBy( _.filter(this.molecularProfilesInStudies.result!, profile=>{ return ((profile.molecularAlterationType === MRNA_EXPRESSION || - profile.molecularAlterationType === PROTEIN_LEVEL) && profile.showProfileInAnalysisTab) || - profile.molecularAlterationType === METHYLATION; + profile.molecularAlterationType === PROTEIN_LEVEL || + profile.molecularAlterationType === GENERIC_ASSAY) && profile.showProfileInAnalysisTab) || + profile.molecularAlterationType === METHYLATION } ), profile=>{ - // Sort order: selected and [mrna, protein, methylation], unselected and [mrna, protein, meth] + // Sort order: selected and [mrna, protein, methylation, treatment], unselected and [mrna, protein, meth, treatment] if (profile.molecularProfileId in selectedMolecularProfileIds) { switch (profile.molecularAlterationType) { case MRNA_EXPRESSION: @@ -2100,15 +2110,19 @@ export class ResultsViewPageStore { return 1; case METHYLATION: return 2; + case GENERIC_ASSAY: + return 3; } } else { switch(profile.molecularAlterationType) { case MRNA_EXPRESSION: - return 3; - case PROTEIN_LEVEL: return 4; - case METHYLATION: + case PROTEIN_LEVEL: return 5; + case METHYLATION: + return 6; + case GENERIC_ASSAY: + return 7; } } } @@ -2280,6 +2294,27 @@ export class ResultsViewPageStore { geneticEntityId: geneset.genesetId, cytoband: "-", geneticEntityData: geneset}); } return Promise.resolve(res); + + } + }); + + readonly treatmentsInStudies = remoteData({ + await:()=>[this.studyIds], + invoke: async () => { + return internalClient.fetchTreatmentsUsingPOST({ + treatmentFilter: { studyIds:this.studyIds.result! } as TreatmentFilter + }) + }, + onResult:(treatments:Treatment[])=>{ + this.treatmentCache.addData(treatments); + } + }); + + readonly selectedTreatments = remoteData({ + await: ()=>[this.treatmentsInStudies], + invoke: () => { + const treatmentIdFromUrl = this.rvQuery.treatmentIds; + return Promise.resolve(_.filter(this.treatmentsInStudies.result!, (d:Treatment) => treatmentIdFromUrl.includes(d.treatmentId))); } }); @@ -2305,6 +2340,23 @@ export class ResultsViewPageStore { } }); + readonly treatmentLinkMap = remoteData<{[treatmentId: string]: string}>({ + invoke: async () => { + if (this.rvQuery.treatmentIds && this.rvQuery.treatmentIds.length) { + const treatments = await internalClient.fetchTreatmentsUsingPOST({ + treatmentFilter: { studyIds:this.studyIds.result! } as TreatmentFilter + }); + const linkMap: {[treatmentId: string]: string} = {}; + treatments.forEach(({treatmentId, refLink}) => { + linkMap[treatmentId] = refLink; + }); + return linkMap; + } else { + return {}; + } + } + }); + readonly customDriverAnnotationReport = remoteData<{ hasBinary:boolean, tiers:string[] }>({ await:()=>[ this.mutations @@ -3107,6 +3159,17 @@ export class ResultsViewPageStore { ) }); + readonly treatmentMolecularDataCache = remoteData({ + await:() => [ + this.molecularProfileIdToDataQueryFilter + ], + invoke: () => Promise.resolve( + new TreatmentMolecularDataCache( + this.molecularProfileIdToDataQueryFilter.result! + ) + ) + }); + @cached get geneCache() { return new GeneCache(); } @@ -3115,6 +3178,10 @@ export class ResultsViewPageStore { return new GenesetCache(); } + @cached get treatmentCache() { + return new TreatmentCache(); + } + public numericGeneMolecularDataCache = new MobxPromiseCache<{entrezGeneId:number, molecularProfileId:string}, NumericGeneMolecularData[]>( q=>({ await: ()=>[ diff --git a/src/pages/resultsView/ResultsViewPageStoreUtils.ts b/src/pages/resultsView/ResultsViewPageStoreUtils.ts index b54fecf8504..04a506cd0a8 100644 --- a/src/pages/resultsView/ResultsViewPageStoreUtils.ts +++ b/src/pages/resultsView/ResultsViewPageStoreUtils.ts @@ -398,7 +398,8 @@ export function getMolecularProfiles(query:any){ query.genetic_profile_ids_PROFILE_COPY_NUMBER_ALTERATION, query.genetic_profile_ids_PROFILE_MRNA_EXPRESSION, query.genetic_profile_ids_PROFILE_PROTEIN_EXPRESSION, - query.genetic_profile_ids_PROFILE_GENESET_SCORE + query.genetic_profile_ids_PROFILE_GENESET_SCORE, + query.genetic_profile_ids_GENERIC_ASSAY ].filter((profile:string|undefined)=>!!profile); // append 'genetic_profile_ids' which is sometimes in use diff --git a/src/pages/resultsView/ResultsViewQuery.ts b/src/pages/resultsView/ResultsViewQuery.ts index 25f127ab8c2..b4282b7165b 100644 --- a/src/pages/resultsView/ResultsViewQuery.ts +++ b/src/pages/resultsView/ResultsViewQuery.ts @@ -15,6 +15,7 @@ export class ResultsViewQuery { @observable public _rppaScoreThreshold:number|undefined; @observable public _zScoreThreshold:number|undefined; @observable public genesetIds:string[] = []; + @observable public treatmentIds:string[] = []; @observable public cohortIdsList:string[] = [];//queried id(any combination of physical and virtual studies) @observable public oqlQuery:string = ""; @@ -103,6 +104,14 @@ export function updateResultsViewQuery( } } + if (urlQuery.treatment_list) { + // we have to trim because for some reason we get a single space from submission + const parsedTreatmentList = urlQuery.treatment_list.trim().length ? (urlQuery.treatment_list.trim().split(/;/)) : []; + if (!_.isEqual(parsedTreatmentList, rvQuery.treatmentIds)) { + rvQuery.treatmentIds = parsedTreatmentList; + } + } + // cohortIdsList will contain virtual study ids (physicalstudies will contain the phsyical studies which comprise the virtual studies) // although resultsViewStore does if (!rvQuery.cohortIdsList || !_.isEqual(_.sortBy(rvQuery.cohortIdsList), _.sortBy(cancerStudyIds))) { diff --git a/src/shared/cache/TreatmentCache.ts b/src/shared/cache/TreatmentCache.ts new file mode 100644 index 00000000000..f792fb720d7 --- /dev/null +++ b/src/shared/cache/TreatmentCache.ts @@ -0,0 +1,24 @@ +import LazyMobXCache from "../lib/LazyMobXCache"; +import {Treatment, TreatmentFilter} from "../api/generated/CBioPortalAPIInternal"; +import internalClient from "../api/cbioportalInternalClientInstance"; + +type Query = { + treatmentId:string; +}; + +function key(o:{treatmentId:string}) { + return o.treatmentId.toUpperCase(); +} + +async function fetch(queries:Query[]) { + return internalClient.fetchTreatmentsUsingPOST({ + treatmentFilter: {treatmentIds: queries.map(q=>q.treatmentId.toUpperCase())} as TreatmentFilter + }); +} + +export default class TreatmentCache extends LazyMobXCache { + + constructor() { + super(key, key, fetch); + } +} diff --git a/src/shared/cache/TreatmentMolecularDataCache.ts b/src/shared/cache/TreatmentMolecularDataCache.ts new file mode 100644 index 00000000000..3c3ce550977 --- /dev/null +++ b/src/shared/cache/TreatmentMolecularDataCache.ts @@ -0,0 +1,103 @@ +import LazyMobXCache, {AugmentedData} from "../lib/LazyMobXCache"; +import {TreatmentMolecularData, TreatmentDataFilterCriteria} from "../api/generated/CBioPortalAPIInternal"; +import client from "shared/api/cbioportalInternalClientInstance"; +import _ from "lodash"; +import {IDataQueryFilter} from "../lib/StoreUtils"; + +export type TreatmentMolecularDataEnhanced = TreatmentMolecularData & { + thresholdType?: ">"|"<"; +}; + +interface IQuery { + treatmentId: string; + molecularProfileId: string; +} + +type SampleFilterByProfile = { + [molecularProfileId: string]: IDataQueryFilter +}; + +function queryToKey(q: IQuery) { + return `${q.molecularProfileId}~${q.treatmentId}`; +} + +function dataToKey(d:TreatmentMolecularData[], q:IQuery) { + return `${q.molecularProfileId}~${q.treatmentId}`; +} + +/** +/* Pairs each IQuery with an (array-wrapped) array of any matching data. +*/ +function augmentQueryResults(queries: IQuery[], results: TreatmentMolecularData[][]) { + const keyedAugments: {[key: string]: AugmentedData} = {}; + for (const query of queries) { + keyedAugments[queryToKey(query)] = { + data: [[]], + meta: query + }; + } + for (const queryResult of results) { + for (let datum of queryResult) { + datum = handleValueThreshold(datum); + keyedAugments[ + queryToKey({ + molecularProfileId: datum.geneticProfileId, + treatmentId: datum.treatmentId + }) + ].data[0].push(datum); + } + } + return _.values(keyedAugments); +} + +// Values are passed as strings from the REST facility +// check for value threshold indicators ('>' or '<') to appear in front of values +// and convert to a numeric value and a separate value threshold indicator +function handleValueThreshold(datum:TreatmentMolecularDataEnhanced):TreatmentMolecularDataEnhanced { + const matches = /([><]?)(.+)/.exec(datum.value); + datum.thresholdType = undefined; + if (matches) { + datum.value = matches[2]; + if (matches[1].length > 0) { + if (matches[1] === '>') { + datum.thresholdType = matches[1] as '>'; + } else if (matches[1] === '<') { + datum.thresholdType = matches[1] as '<'; + } + } + } + + return datum; +} + +async function fetch( + queries:IQuery[], + sampleFilterByProfile: SampleFilterByProfile +): Promise[]> { + const treatmentIdsByProfile = _.mapValues( + _.groupBy(queries, q => q.molecularProfileId), + profileQueries => profileQueries.map(q => q.treatmentId) + ); + const params = Object.keys(treatmentIdsByProfile) + .map(profileId => ({ + geneticProfileId: profileId, + // the Swagger-generated type expected by the client method below + // incorrectly requires both samples and a sample list; + // use 'as' to tell TypeScript that this object really does fit. + // tslint:disable-next-line: no-object-literal-type-assertion + treatmentDataFilterCriteria: { + treatmentIds: treatmentIdsByProfile[profileId], + ...sampleFilterByProfile[profileId] + } as TreatmentDataFilterCriteria + }) + ); + const dataPromises = params.map(param => client.fetchTreatmentGeneticDataItemsUsingPOST(param)); + const results: TreatmentMolecularData[][] = await Promise.all(dataPromises); + return augmentQueryResults(queries, results); +} + +export default class TreatmentMolecularDataCache extends LazyMobXCache{ + constructor(molecularProfileIdToSampleFilter: SampleFilterByProfile) { + super(queryToKey, dataToKey, fetch, molecularProfileIdToSampleFilter); + } +} \ No newline at end of file diff --git a/src/shared/components/oncoprint/DataUtils.spec.ts b/src/shared/components/oncoprint/DataUtils.spec.ts index 5534e3dda8d..4f5bc007c06 100644 --- a/src/shared/components/oncoprint/DataUtils.spec.ts +++ b/src/shared/components/oncoprint/DataUtils.spec.ts @@ -6,7 +6,8 @@ import { import { GeneticTrackDatum, IGeneHeatmapTrackDatum, - IGenesetHeatmapTrackDatum + IGenesetHeatmapTrackDatum, + ITreatmentHeatmapTrackDatum } from "shared/components/oncoprint/Oncoprint"; import {AlterationTypeConstants, AnnotatedExtendedAlteration} from "../../../pages/resultsView/ResultsViewPageStore"; import { @@ -1287,6 +1288,124 @@ describe("DataUtils", ()=>{ {geneset_id:"MY_FAVORITE_GENE_SET-3", study_id:"study", profile_data:7} ); }); + + it('adds thresholdType and category to trackDatum', () => { + let data = [ + {value:8, thresholdType: '>' as '>'}, + ]; + const partialTrackDatum = {}; + fillHeatmapTrackDatum( + partialTrackDatum, + "treatment_id", + "TREATMENT_ID_1", + {patientId:"patient", studyId:"study"} as Sample, + data + ) + assert.deepEqual( + partialTrackDatum, + {treatment_id: "TREATMENT_ID_1", study_id:"study", profile_data:8, thresholdType: '>', category: '>8.00'} + ); + }); + + it('returns smallest value with ASC sort order', () => { + let data = [ + {value:1}, + {value:2}, + {value:3} + ]; + const partialTrackDatum = {}; + fillHeatmapTrackDatum( + partialTrackDatum, + "treatment_id", + "TREATMENT_ID_1", + {patientId:"patient", studyId:"study"} as Sample, + data, + "ASC" + ) + assert.deepEqual( + partialTrackDatum, + {treatment_id: "TREATMENT_ID_1", study_id:"study", profile_data:1} + ); + }); + + it('returns largest value with DESC sort order', () => { + let data = [ + {value:1}, + {value:2}, + {value:3} + ]; + const partialTrackDatum = {}; + fillHeatmapTrackDatum( + partialTrackDatum, + "treatment_id", + "TREATMENT_ID_1", + {patientId:"patient", studyId:"study"} as Sample, + data, + "DESC" + ) + assert.deepEqual( + partialTrackDatum, + {treatment_id: "TREATMENT_ID_1", study_id:"study", profile_data:3} + ); + }); + + it('selects non-threshold over threshold data point when values are equal', () => { + let data = [ + {value:1, thresholdType: '>' as '>'}, + {value:1} + ]; + const partialTrackDatum = {}; + fillHeatmapTrackDatum( + partialTrackDatum, + "treatment_id", + "TREATMENT_ID_1", + {patientId:"patient", studyId:"study"} as Sample, + data + ) + assert.deepEqual( + partialTrackDatum, + {treatment_id: "TREATMENT_ID_1", study_id:"study", profile_data:1} + ); + }); + + it('handles all NaN-value data points', () => { + let data = [ + {value:NaN}, + {value:NaN} + ]; + const partialTrackDatum = {}; + fillHeatmapTrackDatum( + partialTrackDatum, + "treatment_id", + "TREATMENT_ID_1", + {patientId:"patient", studyId:"study"} as Sample, + data, + "DESC" + ) + assert.deepEqual( + partialTrackDatum, + {treatment_id: "TREATMENT_ID_1", study_id:"study", profile_data:NaN} + ); + }); + + it('Prefers largest non-threshold absolute value when no sort order provided', () => { + let data = [ + {value:-10}, + {value:10, thresholdType: '>' as '>'} + ]; + const partialTrackDatum = {}; + fillHeatmapTrackDatum( + partialTrackDatum, + "treatment_id", + "TREATMENT_ID_1", + {patientId:"patient", studyId:"study"} as Sample, + data + ) + assert.deepEqual( + partialTrackDatum, + {treatment_id: "TREATMENT_ID_1", study_id:"study", profile_data:-10} + ); + }); }); describe("fillClinicalTrackDatum", ()=>{ diff --git a/src/shared/components/oncoprint/DataUtils.ts b/src/shared/components/oncoprint/DataUtils.ts index 7380ca24e17..838559dd0cb 100644 --- a/src/shared/components/oncoprint/DataUtils.ts +++ b/src/shared/components/oncoprint/DataUtils.ts @@ -24,6 +24,7 @@ import { MUTATION_STATUS_GERMLINE } from "shared/constants"; import {SpecialAttribute} from "../../cache/ClinicalDataCache"; import {stringListToIndexSet} from "../../../public-lib/lib/StringUtils"; import {isNotGermlineMutation} from "../../lib/MutationUtils"; +import { alphabeticalDefault } from "./SortUtils"; const cnaDataToString:{[integerCNA:string]:string|undefined} = { "-2": "homdel", @@ -59,6 +60,11 @@ const protRenderPriority = { 'low': 0 }; +type HeatmapCaseDatum = { + value:number; + thresholdType?: '<'|'>'; +}; + export type OncoprintMutationType = "missense" | "inframe" | "fusion" | "promoter" | "trunc" | "other"; export function getOncoprintMutationType(d:Pick):OncoprintMutationType { @@ -272,7 +278,8 @@ export function fillHeatmapTrackDatum { - const val = next.value; - if (Math.abs(val) > Math.abs(maxInAbsVal)) { - return val; - } else { - return maxInAbsVal; - } - }, - 0); + // default: the most extreme value (pos. or neg.) is shown for data + // sortOrder=ASC: the smallest value is shown for data + // sortOrder=DESC: the largest value is shown for data + let representingDatum; + switch (sortOrder) { + case "ASC": + let bestValue = _(data).map((d:HeatmapCaseDatum)=>d.value).min(); + representingDatum = selectRepresentingDataPoint(bestValue!, data, false); + break; + case "DESC": + bestValue = _(data).map((d:HeatmapCaseDatum)=>d.value).max(); + representingDatum = selectRepresentingDataPoint(bestValue!, data, false); + break; + default: + bestValue = _.maxBy(data,(d:HeatmapCaseDatum) => Math.abs(d.value))!.value; + representingDatum = selectRepresentingDataPoint(bestValue, data, true); + break; + } + + // `data` can contain data points with only NaN values + // this is detected by `representingDatum` to be undefined + // in that case select the first element as representing datum + if (representingDatum === undefined) { + representingDatum = data[0]; + } + + trackDatum.profile_data = representingDatum!.value; + if (representingDatum!.thresholdType) { + trackDatum.thresholdType = representingDatum!.thresholdType; + trackDatum.category = trackDatum.thresholdType? `${trackDatum.thresholdType}${trackDatum.profile_data.toFixed(2)}` : undefined; + } + } } return trackDatum; } +function selectRepresentingDataPoint(bestValue:number, data:HeatmapCaseDatum[], useAbsolute:boolean):HeatmapCaseDatum { + + const fFilter = useAbsolute? (d:HeatmapCaseDatum) => Math.abs(d.value) === bestValue : (d:HeatmapCaseDatum) => d.value === bestValue; + const selData = _.filter(data, fFilter); + const selDataNoTreshold = _.filter(selData, (d:HeatmapCaseDatum) => !d.thresholdType); + if (selDataNoTreshold.length > 0) { + return selDataNoTreshold[0]; + } else { + return selData[0]; + } +} + export function makeHeatmapTrackData( featureKey: K, featureId: T[K], cases:Sample[]|Patient[], - data: {value: number, uniquePatientKey: string, uniqueSampleKey: string}[] + data: {value: number, uniquePatientKey: string, uniqueSampleKey: string, thresholdType?: ">"|"<"}[], + sortOrder?: string ): T[] { if (!cases.length) { return []; @@ -330,7 +377,7 @@ export function makeHeatmapTrackData{ assert.equal(oncoprint.setSortConfig.callCount, 0); }); }); + + describe('transitionHeatmapTrack() for molecular profile', () => { + + const molecularAlterationType = "MRNA_EXPRESSION"; + + const makeMinimalOncoprintProps = (): IOncoprintProps => ({ + caseLinkOutInTooltips:false, + clinicalTracks: [], + geneticTracks: [], + genesetHeatmapTracks: [], + heatmapTracks: [], + divId: 'myDomId', + width: 1000 + }); + + const nextSpec: IHeatmapTrackSpec = { + key: '', + label: '', + molecularProfileId: "profile_1", + molecularAlterationType: molecularAlterationType, + datatype: "", + trackGroupIndex: 1, + onRemove: () => {}, + data: [ + {profile_data: 1, study_id: "study1", uid: "uid", patient: "patient1"}, + {profile_data: 2, study_id: "study1", uid: "uid", patient: "patient1"}, + {profile_data: 3, study_id: "study1", uid: "uid", patient: "patient1"} + ] + }; + + const prevSpec = undefined; + + const trackspec2trackId = () => { + return { + 'MOLECULARTRACK_1': 1, + 'MOLECULARTRACK_2': 2 + }; + }; + + const nextProps: IOncoprintProps = makeMinimalOncoprintProps(); + const prevProps: IOncoprintProps = makeMinimalOncoprintProps(); + + const oncoprint: OncoprintJS = createStubInstance(OncoprintJS); + + beforeEach(function () { + (oncoprint.shareRuleSet as SinonStub).resetHistory(); + }); + + it('when is new track and ruleSetId is undefined, the trackId is set as ruleSetId', () => { + + const trackIdForRuleSetSharing = {heatmap: undefined}; + + (oncoprint.addTracks as SinonStub).returns([1]); + + transitionHeatmapTrack(nextSpec, prevSpec, trackspec2trackId, ()=>undefined, oncoprint, nextProps, prevProps, trackIdForRuleSetSharing); + + assert.isFalse((oncoprint.shareRuleSet as SinonStub).called); + assert.equal(trackIdForRuleSetSharing.heatmap, 1); + }); + + it('when is new track and ruleSetId is defined, the ruleSetId is shared', () => { + + const trackIdForRuleSetSharing = {heatmap: 1}; + + (oncoprint.addTracks as SinonStub).returns([2]); + + transitionHeatmapTrack(nextSpec, prevSpec, trackspec2trackId, ()=>undefined, oncoprint, nextProps, prevProps, trackIdForRuleSetSharing); + + assert.isTrue((oncoprint.shareRuleSet as SinonStub).called); + assert.equal(trackIdForRuleSetSharing.heatmap, 2); + }); + + it('when is existing track, the ruleSetId is not shared (nothing happens)', () => { + + const trackIdForRuleSetSharing = {heatmap: 1}; + const prevSpec = nextSpec; + + transitionHeatmapTrack(nextSpec, prevSpec, trackspec2trackId, ()=>undefined, oncoprint, nextProps, prevProps, trackIdForRuleSetSharing); + + assert.isFalse((oncoprint.shareRuleSet as SinonStub).called); + assert.equal(trackIdForRuleSetSharing.heatmap, 1); + }); + + }); + + describe('transitionHeatmapTrack() for treatment response profile', () => { + + const molecularAlterationType = "GENERIC_ASSAY"; + + const makeMinimalOncoprintProps = (): IOncoprintProps => ({ + caseLinkOutInTooltips:false, + clinicalTracks: [], + geneticTracks: [], + genesetHeatmapTracks: [], + heatmapTracks: [], + divId: 'myDomId', + width: 1000 + }); + + const nextSpec: IHeatmapTrackSpec = { + key: '', + label: '', + molecularProfileId: "profile_1", + molecularAlterationType: molecularAlterationType, + datatype: "", + trackGroupIndex: 1, + onRemove: () => {}, + data: [ + {profile_data: 1, study_id: "study1", uid: "uid", patient: "patient1"}, + {profile_data: 2, study_id: "study1", uid: "uid", patient: "patient1"}, + {profile_data: 3, study_id: "study1", uid: "uid", patient: "patient1"} + ] + }; + + const prevSpec = undefined; + + const trackspec2trackId = () => { + return { + 'TREATMENT_TRACK_1': 1, + 'TREATMENT_TRACK_2': 2 + }; + }; + + const nextProps: IOncoprintProps = makeMinimalOncoprintProps(); + const prevProps: IOncoprintProps = makeMinimalOncoprintProps(); + + const oncoprint: OncoprintJS = createStubInstance(OncoprintJS); + + beforeEach(function () { + (oncoprint.shareRuleSet as SinonStub).resetHistory(); + }); + + it('when is new track and ruleSetId is undefined, the new trackId is set as ruleSetId', () => { + + const trackIdForRuleSetSharing = {treatment: {} as {[m:string]:number}}; + + (oncoprint.addTracks as SinonStub).returns([1]); + + transitionHeatmapTrack(nextSpec, prevSpec, trackspec2trackId, ()=>undefined, oncoprint, nextProps, prevProps, trackIdForRuleSetSharing); + + assert.isFalse((oncoprint.shareRuleSet as SinonStub).called); + assert.equal(trackIdForRuleSetSharing.treatment['profile_1'], 1); + }); + + it('when is new track and ruleSetId is defined, the new trackId is set as ruleSetId', () => { + + const trackIdForRuleSetSharing = {treatment: { profile_1: 1 }}; + + (oncoprint.addTracks as SinonStub).returns([2]); + + transitionHeatmapTrack(nextSpec, prevSpec, trackspec2trackId, ()=>undefined, oncoprint, nextProps, prevProps, trackIdForRuleSetSharing); + + assert.isFalse((oncoprint.shareRuleSet as SinonStub).called); + assert.equal(trackIdForRuleSetSharing.treatment['profile_1'], 2); + }); + + it('when is existing track and ruleSetId is defined, the ruleset of existing track is updated to ruleSetId', () => { + + const trackIdForRuleSetSharing = {treatment: { profile_1: 2 }}; + const prevSpec = nextSpec; + + transitionHeatmapTrack(nextSpec, prevSpec, trackspec2trackId, ()=>undefined, oncoprint, nextProps, prevProps, trackIdForRuleSetSharing); + + assert.isTrue((oncoprint.shareRuleSet as SinonStub).called); + assert.equal(trackIdForRuleSetSharing.treatment['profile_1'], 2); + }); + + }); }); \ No newline at end of file diff --git a/src/shared/components/oncoprint/DeltaUtils.ts b/src/shared/components/oncoprint/DeltaUtils.ts index b11e58f5a62..27dd4eea83b 100644 --- a/src/shared/components/oncoprint/DeltaUtils.ts +++ b/src/shared/components/oncoprint/DeltaUtils.ts @@ -1,7 +1,7 @@ import { IOncoprintProps, default as Oncoprint, GeneticTrackSpec, IGenesetHeatmapTrackSpec, - IGeneHeatmapTrackSpec, ClinicalTrackSpec, IBaseHeatmapTrackDatum, - CLINICAL_TRACK_GROUP_INDEX, GENETIC_TRACK_GROUP_INDEX + IHeatmapTrackSpec, ClinicalTrackSpec, IBaseHeatmapTrackDatum, ITreatmentHeatmapTrackDatum, + CLINICAL_TRACK_GROUP_INDEX, GENETIC_TRACK_GROUP_INDEX, IGenesetHeatmapTrackDatum, IGeneHeatmapTrackDatum, } from "./Oncoprint"; import OncoprintJS, {TrackId, SortConfig} from "oncoprintjs"; import {ObservableMap} from "mobx"; @@ -16,6 +16,10 @@ import { linebreakGenesetId } from "./TooltipUtils"; import {MolecularProfile} from "../../api/generated/CBioPortalAPI"; +import { AlterationTypeConstants } from "pages/resultsView/ResultsViewPageStore"; +import { isNumberData } from "pages/resultsView/plots/PlotsTabUtils"; +import { isNull, isNumber } from "util"; +import { AnyModifier } from "shared/lib/oql/oql-parser"; export function transition( nextProps:IOncoprintProps, @@ -66,6 +70,9 @@ type TrackSpecsWithDynamicGroups = { heatmapTracks: {trackGroupIndex: number}[], genesetHeatmapTracks: {trackGroupIndex: number}[] }; + +type TreatmentProfileToTrackIdMap = {[molecularProfileId:string]: undefined|TrackId}; + export function transitionTrackGroupSortPriority( nextProps: TrackSpecsWithDynamicGroups, prevProps: Partial, @@ -368,7 +375,8 @@ function transitionTracks( genetic: undefined as undefined|TrackId, genesetHeatmap: undefined as undefined|TrackId, heatmap: undefined as undefined|TrackId, - heatmap01:undefined as undefined|TrackId + heatmap01:undefined as undefined|TrackId, + treatment: {} as any as TreatmentProfileToTrackIdMap }; const trackSpecKeyToTrackId = getTrackSpecKeyToTrackId(); if (prevProps.geneticTracks && prevProps.geneticTracks.length && !hasGeneticTrackRuleSetChanged(nextProps, prevProps)) { @@ -384,9 +392,11 @@ function transitionTracks( let heatmap01; let heatmap; for (const spec of prevProps.heatmapTracks) { - if (heatmap01 === undefined && spec.molecularAlterationType === "METHYLATION") { + if (heatmap01 === undefined && spec.molecularAlterationType === AlterationTypeConstants.METHYLATION) { heatmap01 = trackSpecKeyToTrackId[spec.key]; - } else if (heatmap === undefined) { + } else if (heatmap === undefined + && spec.molecularAlterationType !== AlterationTypeConstants.METHYLATION + && spec.molecularAlterationType !== AlterationTypeConstants.GENERIC_ASSAY) { heatmap = trackSpecKeyToTrackId[spec.key]; } if (heatmap01 !== undefined && heatmap !== undefined) { @@ -405,10 +415,30 @@ function transitionTracks( } } } - + + // collect trackId of last assigned track for each treatment profile + // Note: the resolution of `trackIds for ruleset sharing` is different from + // the section above because different formatting is applied to each treatment profile (molecularProfileId) + trackIdForRuleSetSharing.treatment = _.chain(prevProps.heatmapTracks) + .filter((s:IHeatmapTrackSpec) => s.molecularAlterationType === AlterationTypeConstants.GENERIC_ASSAY) + .groupBy((track:IHeatmapTrackSpec) => track.molecularProfileId) + .mapValues( (o:IHeatmapTrackSpec[]) => _.last(o) ) + .mapValues( (o:IHeatmapTrackSpec) => trackSpecKeyToTrackId[o.key] ) + .value(); + + const treatmentProfilesMap = _.chain(nextProps.heatmapTracks) + .filter((s:IHeatmapTrackSpec) => s.molecularAlterationType === AlterationTypeConstants.GENERIC_ASSAY) + .groupBy((track:IHeatmapTrackSpec) => track.molecularProfileId) + .mapValues( (o:IHeatmapTrackSpec[]) => _(o).flatMap((d)=>d.data).filter((d:IBaseHeatmapTrackDatum) => ! d.category).map((d)=>d.profile_data).value() ) + .value(); + + // find the max and min treatment profile value in the next heatmap track group + // max and min value is used to create a custom legend for the track group + const treatmentProfileMaxValues = _.mapValues(treatmentProfilesMap, (profile_data:number[]) => { return _.max(profile_data) }); + const treatmentProfileMinValues = _.mapValues(treatmentProfilesMap, (profile_data:number[]) => { return _.min(profile_data) }); // Transition genetic tracks - const prevGeneticTracks = _.keyBy(prevProps.geneticTracks || [], track=>track.key); + const prevGeneticTracks = _.keyBy(prevProps.geneticTracks || [], (track:GeneticTrackSpec)=>track.key); for (const track of nextProps.geneticTracks) { transitionGeneticTrack(track, prevGeneticTracks[track.key], getTrackSpecKeyToTrackId, getMolecularProfileMap, oncoprint, nextProps, prevProps, trackIdForRuleSetSharing); @@ -424,10 +454,8 @@ function transitionTracks( // Oncce tracks have been added and deleted, transition order transitionGeneticTrackOrder(nextProps, prevProps, oncoprint, getTrackSpecKeyToTrackId); - - // Transition clinical tracks - const prevClinicalTracks = _.keyBy(prevProps.clinicalTracks || [], track=>track.key); + const prevClinicalTracks = _.keyBy(prevProps.clinicalTracks || [], (track:ClinicalTrackSpec)=>track.key); for (const track of nextProps.clinicalTracks) { transitionClinicalTrack(track, prevClinicalTracks[track.key], getTrackSpecKeyToTrackId, oncoprint, nextProps); delete prevClinicalTracks[track.key]; @@ -440,7 +468,7 @@ function transitionTracks( } // Transition gene set heatmap tracks - const prevGenesetHeatmapTracks = _.keyBy(prevProps.genesetHeatmapTracks || [], track=>track.key); + const prevGenesetHeatmapTracks = _.keyBy(prevProps.genesetHeatmapTracks || [], (track:IGenesetHeatmapTrackSpec)=>track.key); for (const track of nextProps.genesetHeatmapTracks) { transitionGenesetHeatmapTrack(track, prevGenesetHeatmapTracks[track.key], getTrackSpecKeyToTrackId, oncoprint, nextProps, trackIdForRuleSetSharing); @@ -455,17 +483,30 @@ function transitionTracks( } // Transition heatmap tracks - const prevHeatmapTracks = _.keyBy(prevProps.heatmapTracks || [], track=>track.key); - for (const track of nextProps.heatmapTracks) { + const prevHeatmapTracks = _.keyBy(prevProps.heatmapTracks || [], (track:IHeatmapTrackSpec)=>track.key); + for (let track of nextProps.heatmapTracks) { + + // add treatment layout/formatting information to the track specs + track.maxProfileValue = treatmentProfileMaxValues[track.molecularProfileId]; + track.minProfileValue = treatmentProfileMinValues[track.molecularProfileId]; + transitionHeatmapTrack(track, prevHeatmapTracks[track.key], getTrackSpecKeyToTrackId, - () => undefined, oncoprint, nextProps, {}, trackIdForRuleSetSharing); + () => undefined, oncoprint, nextProps, {}, trackIdForRuleSetSharing, + undefined); delete prevHeatmapTracks[track.key]; } + for (const track of (prevProps.heatmapTracks || [])) { + // if its still there, then this track no longer exists if (prevHeatmapTracks.hasOwnProperty(track.key)) { - // if its still there, then this track no longer exists + + // add treatment layout/formatting information to the track specs + track.maxProfileValue = treatmentProfileMaxValues[track.molecularProfileId]; + track.minProfileValue = treatmentProfileMinValues[track.molecularProfileId]; + transitionHeatmapTrack(undefined, prevHeatmapTracks[track.key], getTrackSpecKeyToTrackId, - () => undefined, oncoprint, nextProps, {}, trackIdForRuleSetSharing); + () => undefined, oncoprint, nextProps, {}, trackIdForRuleSetSharing, + undefined); } } } @@ -557,7 +598,7 @@ function updateExpansionTracks< ? nextParentSpec.expansionTrackList : [] ); - const prevExpansionTracks = _.keyBy(expansionTrackList, track => track.key); + const prevExpansionTracks = _.keyBy(expansionTrackList, (track:TrackSpecType) => track.key); for (const track of nextExpansionTracks) { // nextParentSpec cannot be undefined, or we wouldn't have entered // this loop @@ -755,7 +796,7 @@ function transitionGenesetHeatmapTrack( const trackSpecKeyToTrackId = getTrackSpecKeyToTrackId(); if (tryRemoveTrack(nextSpec, prevSpec, trackSpecKeyToTrackId, oncoprint)) { - updateExpansionTracks( + updateExpansionTracks( undefined, prevSpec, getTrackSpecKeyToTrackId, () => undefined, @@ -795,7 +836,7 @@ function transitionGenesetHeatmapTrack( oncoprint.shareRuleSet(trackIdForRuleSetSharing.genesetHeatmap, newTrackId); } trackIdForRuleSetSharing.genesetHeatmap = newTrackId; - updateExpansionTracks( + updateExpansionTracks( nextSpec, undefined, getTrackSpecKeyToTrackId, () => undefined, @@ -815,7 +856,7 @@ function transitionGenesetHeatmapTrack( } // set tooltip, its cheap oncoprint.setTrackTooltipFn(trackId, makeHeatmapTrackTooltip(nextSpec.molecularAlterationType, true)); - updateExpansionTracks( + updateExpansionTracks( nextSpec, prevSpec, getTrackSpecKeyToTrackId, () => undefined, @@ -827,15 +868,16 @@ function transitionGenesetHeatmapTrack( ); } } -function transitionHeatmapTrack( - nextSpec:IGeneHeatmapTrackSpec|undefined, - prevSpec:IGeneHeatmapTrackSpec|undefined, + +export function transitionHeatmapTrack( + nextSpec:IHeatmapTrackSpec|undefined, + prevSpec:IHeatmapTrackSpec|undefined, getTrackSpecKeyToTrackId:()=>{[key:string]:TrackId}, getMolecularProfileMap: () => (object | undefined), oncoprint:OncoprintJS, nextProps:IOncoprintProps, prevProps:object, - trackIdForRuleSetSharing:{heatmap?:TrackId, heatmap01?:TrackId}, + trackIdForRuleSetSharing:{heatmap?:TrackId, heatmap01?:TrackId, treatment?: TreatmentProfileToTrackIdMap}, expansionParentKey?:string ) { const trackSpecKeyToTrackId = getTrackSpecKeyToTrackId(); @@ -844,7 +886,7 @@ function transitionHeatmapTrack( } else if (nextSpec && !prevSpec) { // Add track const heatmapTrackParams = { - rule_set_params: getHeatmapTrackRuleSetParams(nextSpec.molecularAlterationType), + rule_set_params: getHeatmapTrackRuleSetParams(nextSpec), data: nextSpec.data, data_id_key: "uid", has_column_spacing: false, @@ -860,6 +902,7 @@ function transitionHeatmapTrack( sort_direction_changeable: true, sortCmpFn: heatmapTrackSortComparator, init_sort_direction: 0 as 0, + link_url: nextSpec.trackLinkUrl, description: `${nextSpec.label} data from ${nextSpec.molecularProfileId}`, tooltipFn: makeHeatmapTrackTooltip(nextSpec.molecularAlterationType, true), track_info: nextSpec.info || "", @@ -870,20 +913,31 @@ function transitionHeatmapTrack( : undefined ) }; - const newTrackId = oncoprint.addTracks([heatmapTrackParams])[0]; + // register new track in oncoprint + const newTrackId:number = oncoprint.addTracks([heatmapTrackParams])[0]; + // store relation between React heatmap track specs and OncoprintJS trackIds trackSpecKeyToTrackId[nextSpec.key] = newTrackId; - let trackIdForRuleSetSharingKey:"heatmap"|"heatmap01" = "heatmap"; - if (nextSpec.molecularAlterationType === "METHYLATION") { - trackIdForRuleSetSharingKey = "heatmap01"; - } - if (typeof trackIdForRuleSetSharing[trackIdForRuleSetSharingKey] !== "undefined") { - oncoprint.shareRuleSet(trackIdForRuleSetSharing[trackIdForRuleSetSharingKey]!, newTrackId); + if (nextSpec.molecularAlterationType !== AlterationTypeConstants.GENERIC_ASSAY) { + let trackIdForRuleSetSharingKey:"heatmap"|"heatmap01" = "heatmap"; + if (nextSpec.molecularAlterationType === "METHYLATION") { + trackIdForRuleSetSharingKey = "heatmap01"; + } + if (typeof trackIdForRuleSetSharing[trackIdForRuleSetSharingKey] !== "undefined") { + oncoprint.shareRuleSet(trackIdForRuleSetSharing[trackIdForRuleSetSharingKey]!, newTrackId); + } + trackIdForRuleSetSharing[trackIdForRuleSetSharingKey] = newTrackId; + } else { + // if the track is a treatment profile, add to trackIdForRuleSetSharing under its `molecularProfileId` + // this makes the trackId available for existing tracks of the same mol.profile for ruleset sharing + trackIdForRuleSetSharing.treatment![nextSpec.molecularProfileId] = newTrackId; } - trackIdForRuleSetSharing[trackIdForRuleSetSharingKey] = newTrackId; + } else if (nextSpec && prevSpec) { // Transition track const trackId = trackSpecKeyToTrackId[nextSpec.key]; + // when the data in the next track differs from the previous + // register the new data points in oncoprint if (nextSpec.data !== prevSpec.data) { // shallow equality check oncoprint.setTrackData(trackId, nextSpec.data, "uid"); @@ -891,7 +945,13 @@ function transitionHeatmapTrack( if (nextSpec.info !== prevSpec.info && nextSpec.info !== undefined) { oncoprint.setTrackInfo(trackId, nextSpec.info); } + // treatment profile tracks always are associated with the last added added track id + if (nextSpec.molecularAlterationType === AlterationTypeConstants.GENERIC_ASSAY + && trackIdForRuleSetSharing.treatment![nextSpec.molecularProfileId] !== undefined) { + const rulesetTrackId = trackIdForRuleSetSharing.treatment![nextSpec.molecularProfileId]; + oncoprint.shareRuleSet(rulesetTrackId!, trackId); + } // set tooltip, its cheap oncoprint.setTrackTooltipFn(trackId, makeHeatmapTrackTooltip(nextSpec.molecularAlterationType, true)); } -} \ No newline at end of file +} diff --git a/src/shared/components/oncoprint/Oncoprint.tsx b/src/shared/components/oncoprint/Oncoprint.tsx index 62dd6dcd164..344841d114e 100644 --- a/src/shared/components/oncoprint/Oncoprint.tsx +++ b/src/shared/components/oncoprint/Oncoprint.tsx @@ -51,6 +51,8 @@ export interface IBaseHeatmapTrackDatum { study_id: string; uid: string; na?:boolean; + category?:string; + thresholdType?:">"|"<"; } export interface IGeneHeatmapTrackDatum extends IBaseHeatmapTrackDatum { hugo_gene_symbol: string; @@ -58,6 +60,9 @@ export interface IGeneHeatmapTrackDatum extends IBaseHeatmapTrackDatum { export interface IGenesetHeatmapTrackDatum extends IBaseHeatmapTrackDatum { geneset_id: string; } +export interface ITreatmentHeatmapTrackDatum extends IBaseHeatmapTrackDatum { + treatment_id: string; +} export type GeneticTrackDatum_Data = Pick void; + +export interface IHeatmapTrackSpec extends IBaseHeatmapTrackSpec { + data: IBaseHeatmapTrackDatum[]; // can be IGeneHeatmapTrackDatum or ITreatmentHeatmapTrackDatum info?: string; labelColor?: string; + trackLinkUrl?: string | undefined; + onRemove: () => void; + molecularProfileName?: string; + pivotThreshold?: number; + sortOrder?: string; + maxProfileValue?: number; + minProfileValue?: number; } export interface IGenesetHeatmapTrackSpec extends IBaseHeatmapTrackSpec { data: IGenesetHeatmapTrackDatum[]; trackLinkUrl: string | undefined; - expansionTrackList: IGeneHeatmapTrackSpec[]; + expansionTrackList: IHeatmapTrackSpec[]; expansionCallback: () => void; } @@ -131,7 +143,7 @@ export interface IOncoprintProps { geneticTracks: GeneticTrackSpec[]; geneticTracksOrder?:string[]; // track keys genesetHeatmapTracks: IGenesetHeatmapTrackSpec[]; - heatmapTracks: IGeneHeatmapTrackSpec[]; + heatmapTracks: IHeatmapTrackSpec[]; divId:string; width:number; caseLinkOutInTooltips:boolean; diff --git a/src/shared/components/oncoprint/OncoprintUtils.spec.ts b/src/shared/components/oncoprint/OncoprintUtils.spec.ts index 929b02bdf04..11e8ff18b85 100644 --- a/src/shared/components/oncoprint/OncoprintUtils.spec.ts +++ b/src/shared/components/oncoprint/OncoprintUtils.spec.ts @@ -1,12 +1,19 @@ import { alterationInfoForCaseAggregatedDataByOQLLine, makeGeneticTrackWith, - percentAltered + percentAltered, + extractTreatmentSelections, + getTreatmentTrackRuleSetParams } from "./OncoprintUtils"; import {observable} from "mobx"; import * as _ from 'lodash'; import {assert} from 'chai'; import {IQueriedMergedTrackCaseData} from "../../../pages/resultsView/ResultsViewPageStore"; +import { splitHeatmapTextField } from 'shared/components/oncoprint/OncoprintUtils'; +import { ISelectOption } from 'shared/components/oncoprint/controls/OncoprintControls'; +import { IHeatmapTrackSpec, IBaseHeatmapTrackDatum } from "./Oncoprint"; +import { IGradientAndCategoricalRuleSetParams } from "oncoprintjs"; +import { isMutationProfile } from "shared/lib/StoreUtils"; describe('OncoprintUtils', () => { describe('alterationInfoForCaseAggregatedDataByOQLLine', () => { @@ -392,4 +399,197 @@ describe('OncoprintUtils', () => { assert.equal(percentAltered(2,100), "2%"); }) }); + + describe('Treatment ruleset params', () => { + + it('Is created from Track Spec param', () => { + const treatmentTracSpec = { + key: 'TREATMENTTRACK_1', + label: '', + molecularProfileId: "profile1", + molecularAlterationType: "GENERIC_ASSAY", + data: [ + {profile_data: 1, study: "study1", uid: "uid"}, + {profile_data: 2, study: "study1", uid: "uid"}, + {profile_data: 3, study: "study1", uid: "uid"} + ], + datatype: "GENERIC_ASSAY", + trackGroupIndex: 1, + onRemove: () => {} + }; + + // const ruleSetParams = getTreatmentTrackRuleSetParams(treatmentTracSpec); + + }); + }); + +}); + +describe('splitHeatmapTextField', () => { + it('Splits around spaces', () => { + const elements = splitHeatmapTextField("A B C"); + assert.deepEqual(elements, ["A", "B", "C"]); + }); + it('Splits around tabs', () => { + const elements = splitHeatmapTextField("A B C"); + assert.deepEqual(elements, ["A", "B", "C"]); + }); + it("Splits around comma's", () => { + const elements = splitHeatmapTextField("A,B,C"); + assert.deepEqual(elements, ["A", "B", "C"]); + }); + it("Splits around comma's and spaces", () => { + const elements = splitHeatmapTextField("A, B, C"); + assert.deepEqual(elements, ["A", "B", "C"]); + }); + it("Splits around comma's and tabs", () => { + const elements = splitHeatmapTextField("A, B, C"); + assert.deepEqual(elements, ["A", "B", "C"]); + }); + it("Splits around EOL's", () => { + const elements = splitHeatmapTextField(` + A + B + C`); + assert.deepEqual(elements, ["A", "B", "C"]); + }); + it("Removes duplicate entries", () => { + const elements = splitHeatmapTextField("A B B"); + assert.deepEqual(elements, ["A", "B"]); + }); +}); + +describe('getTreatmentTrackRuleSetParams', () => { + + const treatmentTracSpec = { + key: 'TREATMENTTRACK_1', + label: '', + molecularProfileId: "profile_1", + molecularAlterationType: "GENERIC_ASSAY", + data: [ + {profile_data: 1, study: "study1", uid: "uid"}, + {profile_data: 2, study: "study1", uid: "uid"}, + {profile_data: 3, study: "study1", uid: "uid"} + ] as any as IBaseHeatmapTrackDatum[], + datatype: "GENERIC_ASSAY", + trackGroupIndex: 1, + maxProfileValue: 100, + minProfileValue: -100, + onRemove: () => {} + } as any as IHeatmapTrackSpec; + + it('RuleSetParams are correct w/o sortOrder and pivotThreshold provided', () => { + + const {value_range, colors, value_stop_points, category_to_color} = getTreatmentTrackRuleSetParams(treatmentTracSpec) as IGradientAndCategoricalRuleSetParams; + + assert.equal(colors!.length, 2); + assert.deepEqual(value_range, [-100, 100]); + assert.deepEqual(value_stop_points, [-100, 100]); + assert.deepEqual(category_to_color, undefined); + + }); + + it('ASC SortOrder is default', () => { + + const spec = Object.assign({}, treatmentTracSpec); + spec.sortOrder = "ASC"; + + const {value_range, colors, value_stop_points, category_to_color} = getTreatmentTrackRuleSetParams(treatmentTracSpec) as IGradientAndCategoricalRuleSetParams; + + assert.deepEqual(value_range, [-100, 100]); + + }); + + it('DESC SortOrder reverses value range', () => { + + const spec = Object.assign({}, treatmentTracSpec); + spec.sortOrder = "DESC"; + + const {value_range, colors, value_stop_points, category_to_color} = getTreatmentTrackRuleSetParams(spec) as IGradientAndCategoricalRuleSetParams; + + assert.deepEqual(value_range, [100, -100]); + + }); + + it('DESC SortOrder reverses value_range and value_stop_points', () => { + + const spec = Object.assign({}, treatmentTracSpec); + spec.sortOrder = "DESC"; + + const {value_range, colors, value_stop_points, category_to_color} = getTreatmentTrackRuleSetParams(spec) as IGradientAndCategoricalRuleSetParams; + + assert.deepEqual(value_range, [100, -100]); + assert.deepEqual(value_stop_points, [100, -100]); + + }); + + it('PivotThreshold adds middle value and color param', () => { + + const spec = Object.assign({}, treatmentTracSpec); + spec.pivotThreshold = 0; + + const {value_range, colors, value_stop_points, category_to_color} = getTreatmentTrackRuleSetParams(spec) as IGradientAndCategoricalRuleSetParams; + + assert.equal(colors!.length, 4); + assert.deepEqual(value_range, [-100, 100]); + assert.deepEqual(value_stop_points, [-100, 0, 0, 100]); + + }); + + it('PivotThreshold to the left of min and max profile values is correctly integrated', () => { + + const spec = Object.assign({}, treatmentTracSpec); + spec.pivotThreshold = -200; + + const {value_range, colors, value_stop_points, category_to_color} = getTreatmentTrackRuleSetParams(spec) as IGradientAndCategoricalRuleSetParams; + + assert.equal(colors!.length, 2); + assert.deepEqual(value_range, [-200, 100]); + assert.deepEqual(value_stop_points, [-200, 100]); + + }); + + it('PivotThreshold to the right of min and max profile values is correctly integrated', () => { + + const spec = Object.assign({}, treatmentTracSpec); + spec.pivotThreshold = 200; + + const {value_range, colors, value_stop_points, category_to_color} = getTreatmentTrackRuleSetParams(spec) as IGradientAndCategoricalRuleSetParams; + + assert.equal(colors!.length, 2); + assert.deepEqual(value_range, [-100, 200]); + assert.deepEqual(value_stop_points, [-100, 200]); + + }); + + it('Categories are added when present on data points', () => { + + const spec = Object.assign({}, treatmentTracSpec); + spec.data = [{profile_data: 3, study: "study1", uid: "uid", category: ">8.00"}] as any as IBaseHeatmapTrackDatum[]; + + const {value_range, colors, value_stop_points, category_to_color} = getTreatmentTrackRuleSetParams(spec) as IGradientAndCategoricalRuleSetParams; + + assert.isDefined(category_to_color); + assert.isString(category_to_color!['>8.00']); + + }); +}); + +describe('extractTreatmentSelections', () => { + + const selectedTreatments:ISelectOption[] = []; + const treatmentMap = { + 'treatmentA': {id: 'treatmentA', value: 'valueA', label: 'labelA'} + }; + + it('Adds recognized treatments to selection', () => { + extractTreatmentSelections("treatmentA treatmentB", selectedTreatments, treatmentMap); + assert.deepEqual(selectedTreatments, [{id: 'treatmentA', value: 'valueA', label: 'labelA'}]); + }); + + it('Removed recognized treatments from text field', () => { + const text = extractTreatmentSelections('treatmentC treatmentA treatmentB', selectedTreatments, treatmentMap); + assert.equal(text, 'treatmentC treatmentB'); + }); + }); \ No newline at end of file diff --git a/src/shared/components/oncoprint/OncoprintUtils.ts b/src/shared/components/oncoprint/OncoprintUtils.ts index 90e8d60474f..8721754d50f 100644 --- a/src/shared/components/oncoprint/OncoprintUtils.ts +++ b/src/shared/components/oncoprint/OncoprintUtils.ts @@ -2,10 +2,12 @@ import OncoprintJS, {IGeneticAlterationRuleSetParams, RuleSetParams, TrackSortCo import { ClinicalTrackSpec, GeneticTrackDatum, GeneticTrackSpec, + IHeatmapTrackSpec, IGeneHeatmapTrackDatum, - IGeneHeatmapTrackSpec, + ITreatmentHeatmapTrackDatum, IGenesetHeatmapTrackDatum, IGenesetHeatmapTrackSpec, + IBaseHeatmapTrackDatum } from "./Oncoprint"; import { genetic_rule_set_same_color_for_all_no_recurrence, @@ -47,9 +49,14 @@ import { } from "../../cache/ClinicalDataCache"; import {STUDY_VIEW_CONFIG} from "../../../pages/studyView/StudyViewConfig"; import {getClinicalValueColor, RESERVED_CLINICAL_VALUE_COLORS} from "shared/lib/Colors"; +import GeneMolecularDataCache from "shared/cache/GeneMolecularDataCache"; +import { AlterationTypes } from "pages/patientView/copyNumberAlterations/column/CnaColumnFormatter"; +import { isNumber } from "util"; +import { ISelectOption } from "./controls/OncoprintControls"; +import {TreatmentMolecularDataEnhanced} from "shared/cache/TreatmentMolecularDataCache.ts"; interface IGenesetExpansionMap { - [genesetTrackKey: string]: IGeneHeatmapTrackSpec[]; + [genesetTrackKey: string]: IHeatmapTrackSpec[]; } function makeGenesetHeatmapExpandHandler( @@ -144,22 +151,30 @@ export function doWithRenderingSuppressedAndSortingOff(oncoprint:OncoprintJS= rightBoundaryValue) { + // when data points do not bracket the pivotThreshold, make an artificial right boundary + value_stop_points = [leftBoundaryValue, pivotThreshold, pivotThreshold, pivotThreshold+(pivotThreshold-leftBoundaryValue)]; + } else { + value_stop_points = [leftBoundaryValue, pivotThreshold, pivotThreshold, rightBoundaryValue]; + } + } + + if (sortOrder === "DESC") { // smaller concentrations are `better` (DESC) + value_range = _.reverse(value_range); + value_stop_points = _.reverse(value_stop_points); + } + + let counter = 0; + const categories = _(dataPoints as ITreatmentHeatmapTrackDatum[]).filter((d:ITreatmentHeatmapTrackDatum) => !!d.category).map((d)=>d.category).uniq().value(); + categories.forEach( (d:string) => { + if (category_to_color === undefined) { + category_to_color = {}; + } + category_to_color![d] = categoryColorOptions[counter++]; + if (counter === categoryColorOptions.length) { + counter = 0; + } + }); + + return { + type: 'gradient+categorical' as 'gradient+categorical', + legend_label, + value_key: "profile_data", + value_range, + colors, + value_stop_points, + null_color: 'rgba(224,224,224,1)', + category_key: "category", + category_to_color: category_to_color + }; +} + export function getGenesetHeatmapTrackRuleSetParams() { return { type: 'gradient' as 'gradient', @@ -555,7 +660,7 @@ export function makeClinicalTracksMobxPromise(oncoprint:ResultsViewOncoprint, sa } export function makeHeatmapTracksMobxPromise(oncoprint:ResultsViewOncoprint, sampleMode:boolean) { - return remoteData({ + return remoteData({ await:()=>[ oncoprint.props.store.samples, oncoprint.props.store.patients, @@ -566,12 +671,13 @@ export function makeHeatmapTracksMobxPromise(oncoprint:ResultsViewOncoprint, sam const molecularProfileIdToMolecularProfile = oncoprint.props.store.molecularProfileIdToMolecularProfile.result!; const molecularProfileIdToHeatmapTracks = oncoprint.molecularProfileIdToHeatmapTracks; - const neededGenes = _.flatten(molecularProfileIdToHeatmapTracks.values().map(v=>v.genes.keys())); - const genes = await oncoprint.props.store.geneCache.getPromise(neededGenes.map(g=>({hugoGeneSymbol:g})), true); + const geneProfiles = _.filter(molecularProfileIdToHeatmapTracks.values(), d => d.molecularAlterationType !== AlterationTypeConstants.GENERIC_ASSAY); + const neededGenes = _.flatten(geneProfiles.map(v=>v.entities.keys())); + await oncoprint.props.store.geneCache.getPromise(neededGenes.map(g=>({hugoGeneSymbol:g})), true); - const cacheQueries = _.flatten(molecularProfileIdToHeatmapTracks.entries().map(entry=>( - entry[1].genes.keys().map(g=>({ - molecularProfileId: entry[0], + const cacheQueries = _.flatten(geneProfiles.map(entry=>( + entry.entities.keys().map(g=>({ + molecularProfileId: entry.molecularProfileId, entrezGeneId: oncoprint.props.store.geneCache.get({ hugoGeneSymbol:g })!.data!.entrezGeneId, hugoGeneSymbol: g.toUpperCase() })) @@ -585,6 +691,7 @@ export function makeHeatmapTracksMobxPromise(oncoprint:ResultsViewOncoprint, sam const molecularProfileId = query.molecularProfileId; const gene = query.hugoGeneSymbol; const data = oncoprint.props.store.geneMolecularDataCache.result!.get(query)!.data!; + return { key: `HEATMAPTRACK_${molecularProfileId},${gene}`, label: gene, @@ -601,8 +708,8 @@ export function makeHeatmapTracksMobxPromise(oncoprint:ResultsViewOncoprint, sam onRemove:action(()=>{ const trackGroup = molecularProfileIdToHeatmapTracks.get(molecularProfileId); if (trackGroup) { - trackGroup.genes.delete(gene); - if (!trackGroup.genes.size) { + trackGroup.entities.delete(gene); + if (!trackGroup.entities.size) { molecularProfileIdToHeatmapTracks.delete(molecularProfileId); } } @@ -620,6 +727,89 @@ export function makeHeatmapTracksMobxPromise(oncoprint:ResultsViewOncoprint, sam }); } +export function makeTreatmentProfileHeatmapTracksMobxPromise(oncoprint:ResultsViewOncoprint, sampleMode:boolean) { + return remoteData({ + await:()=>[ + oncoprint.props.store.samples, + oncoprint.props.store.patients, + oncoprint.props.store.molecularProfileIdToMolecularProfile, + oncoprint.props.store.treatmentMolecularDataCache, + oncoprint.props.store.treatmentLinkMap + ], + invoke:async()=>{ + + const molecularProfileIdToMolecularProfile = oncoprint.props.store.molecularProfileIdToMolecularProfile.result!; + const molecularProfileIdToHeatmapTracks = oncoprint.molecularProfileIdToHeatmapTracks; + const treatmentLinkMap = oncoprint.props.store.treatmentLinkMap.result!; + + const treatmentProfiles = _.filter(molecularProfileIdToHeatmapTracks.values(), d => d.molecularAlterationType === AlterationTypeConstants.GENERIC_ASSAY); + const neededTreatments = _.flatten(treatmentProfiles.map(v=>v.entities.keys())); + await oncoprint.props.store.treatmentCache.getPromise(neededTreatments.map(g=>({treatmentId:g})), true); + + const cacheQueries = _.flatten(treatmentProfiles.map(entry=>( + entry.entities.keys().map(g=>({ + molecularProfileId: entry.molecularProfileId, + treatmentId: oncoprint.props.store.treatmentCache.get({ treatmentId:g })!.data!.treatmentId, + treatmentName: oncoprint.props.store.treatmentCache.get({ treatmentId:g })!.data!.name + })) + ))); + + await oncoprint.props.store.treatmentMolecularDataCache.result!.getPromise(cacheQueries, true); + + const samples = oncoprint.props.store.samples.result!; + const patients = oncoprint.props.store.patients.result!; + + const tracks = cacheQueries.map(query=>{ + + const molecularProfileId = query.molecularProfileId; + const profile = molecularProfileIdToMolecularProfile[molecularProfileId]; + const dataCache = oncoprint.props.store.treatmentMolecularDataCache.result!; + + const treatmentId = query.treatmentId; + const pivotThreshold = profile.pivotThreshold; + const sortOrder = profile.sortOrder; + + return { + key: `TREATMENTHEATMAPTRACK_${molecularProfileId},${treatmentId}`, + label: query.treatmentName, + molecularProfileId: query.molecularProfileId, + molecularProfileName: molecularProfileIdToMolecularProfile[molecularProfileId].name, + molecularAlterationType: molecularProfileIdToMolecularProfile[molecularProfileId].molecularAlterationType, + datatype: molecularProfileIdToMolecularProfile[molecularProfileId].datatype, + data: makeHeatmapTrackData( + 'treatment_id', + treatmentId, + sampleMode ? samples : patients, + dataCache.get(query)!.data!.map(d => ({...d, value: parseFloat(d.value)})), + sortOrder + ), + pivotThreshold: pivotThreshold, + sortOrder: sortOrder, + trackLinkUrl: treatmentLinkMap[treatmentId], + trackGroupIndex: molecularProfileIdToHeatmapTracks.get(molecularProfileId)!.trackGroupIndex, + onRemove:action(()=>{ + const trackGroup = molecularProfileIdToHeatmapTracks.get(molecularProfileId); + if (trackGroup) { + trackGroup.entities.delete(treatmentId); + if (!trackGroup.entities.size) { + molecularProfileIdToHeatmapTracks.delete(molecularProfileId); + } + } + if (!molecularProfileIdToHeatmapTracks.has(molecularProfileId) + && oncoprint.sortMode.type === "heatmap" + && oncoprint.sortMode.clusteredHeatmapProfile === molecularProfileId + ) { + oncoprint.sortByData(); + } + }) + }; + }); + return tracks; + }, + default: [] + }); +} + export function makeGenesetHeatmapExpansionsMobxPromise(oncoprint:ResultsViewOncoprint, sampleMode:boolean) { return remoteData({ await: () => [ @@ -646,7 +836,7 @@ export function makeGenesetHeatmapExpansionsMobxPromise(oncoprint:ResultsViewOnc .map(({entrezGeneId, molecularProfileId}) => ({entrezGeneId, molecularProfileId})); await dataCache.getPromise(cacheQueries, true); - const tracksByGenesetTrack: {[genesetTrackKey: string]: IGeneHeatmapTrackSpec[]} = {}; + const tracksByGenesetTrack: {[genesetTrackKey: string]: IHeatmapTrackSpec[]} = {}; expansionsByGenesetTrack.entries().forEach( ([gsTrack, genes]) => { tracksByGenesetTrack[gsTrack] = genes.map( @@ -752,3 +942,37 @@ export function makeGenesetHeatmapTracksMobxPromise( default: [] }); } + +export function extractTreatmentSelections(text:string, selectedTreatments:ISelectOption[], treatmentsMap:{[treatmentId:string]:ISelectOption}):string { + + // get values from input string + const elements = splitHeatmapTextField(text); + + // check values for valid treatment ids + const selectedKeys:string[] = _.map(selectedTreatments,'id'); + const detectedTreatments:string[] = []; + _.each(elements, (d:string)=> { + if (d in treatmentsMap) { + detectedTreatments.push(d); + if (! selectedKeys.includes(d)) { + selectedTreatments.push( treatmentsMap[d] ); + } + } + }); + + // remove valid treatment ids from the input string + if (detectedTreatments.length > 0) { + _.each(detectedTreatments, (d:string) => { + text = text.replace(d, ""); + }); + } + + // return the input string + return text.trim().replace("\t+","\t").replace(" +"," "); + +} + +export function splitHeatmapTextField(text:string):string[] { + text = text.replace(/[,\s\n]+/g," ").trim(); + return _.uniq(text.split(/[,\s\n]+/)); +} \ No newline at end of file diff --git a/src/shared/components/oncoprint/ResultsViewOncoprint.spec.tsx b/src/shared/components/oncoprint/ResultsViewOncoprint.spec.tsx index 3f5fc46b282..1fb525c3e43 100644 --- a/src/shared/components/oncoprint/ResultsViewOncoprint.spec.tsx +++ b/src/shared/components/oncoprint/ResultsViewOncoprint.spec.tsx @@ -31,7 +31,8 @@ describe('Oncoprint sortBy URL parameter', () => { const storeMock = { samples: {isComplete: true, result: samples}, patients: {isComplete: true, result: patients}, - givenSampleOrder: {isComplete: true, result: caseList} + givenSampleOrder: {isComplete: true, result: caseList}, + molecularProfileIdToMolecularProfile: {isComplete: true} } as any as ResultsViewPageStore; it('`case_id` provides sorted sample config to oncoprint', () => { diff --git a/src/shared/components/oncoprint/ResultsViewOncoprint.tsx b/src/shared/components/oncoprint/ResultsViewOncoprint.tsx index 06583b40273..c52a8af1b08 100644 --- a/src/shared/components/oncoprint/ResultsViewOncoprint.tsx +++ b/src/shared/components/oncoprint/ResultsViewOncoprint.tsx @@ -2,13 +2,13 @@ import * as React from "react"; import {Observer, observer} from "mobx-react"; import {action, computed, IObservableObject, IReactionDisposer, observable, ObservableMap, reaction} from "mobx"; import {remoteData} from "public-lib/api/remoteData"; -import Oncoprint, {GENETIC_TRACK_GROUP_INDEX} from "./Oncoprint"; +import Oncoprint, {GENETIC_TRACK_GROUP_INDEX, IHeatmapTrackSpec} from "./Oncoprint"; import OncoprintControls, { IOncoprintControlsHandlers, IOncoprintControlsState } from "shared/components/oncoprint/controls/OncoprintControls"; -import {ResultsViewPageStore} from "../../../pages/resultsView/ResultsViewPageStore"; import {Gene, MolecularProfile, Sample} from "../../api/generated/CBioPortalAPI"; +import {ResultsViewPageStore, AlterationTypeConstants} from "../../../pages/resultsView/ResultsViewPageStore"; import { getAlteredUids, getUnalteredUids, @@ -16,7 +16,8 @@ import { makeGenesetHeatmapExpansionsMobxPromise, makeGenesetHeatmapTracksMobxPromise, makeGeneticTracksMobxPromise, - makeHeatmapTracksMobxPromise + makeHeatmapTracksMobxPromise, + makeTreatmentProfileHeatmapTracksMobxPromise } from "./OncoprintUtils"; import _ from "lodash"; import onMobxPromise from "shared/lib/onMobxPromise"; @@ -34,6 +35,7 @@ import {getAnnotatingProgressMessage} from "./ResultsViewOncoprintUtils"; import ProgressIndicator, {IProgressIndicatorItem} from "../progressIndicator/ProgressIndicator"; import autobind from "autobind-decorator"; import getBrowserWindow from "../../lib/getBrowserWindow"; +import { MobxPromiseUnionTypeWithDefault } from "mobxpromise"; interface IResultsViewOncoprintProps { divId: string; @@ -65,14 +67,20 @@ export const SAMPLE_MODE_URL_PARAM = "show_samples"; export const CLINICAL_TRACKS_URL_PARAM = "clinicallist"; export const HEATMAP_TRACKS_URL_PARAM = "heatmap_track_groups"; export const ONCOPRINT_SORTBY_URL_PARAM = "oncoprint_sortby"; +export const TREATMENT_LIST_URL_PARAM = "treatment_list"; const CLINICAL_TRACK_KEY_PREFIX = "CLINICALTRACK_"; +/* Each heatmap track group can hold tracks of a single entity type. + Implemented entity types are genes and treatments. In the + HeatmapTrackGroupRecord type the `entities` member refers to + hugo_gene_symbols (for genes) or to treatment_id's (for treatments). */ type HeatmapTrackGroupRecord = { trackGroupIndex:number, - genes:ObservableMap, + molecularAlterationType:string, + entities:ObservableMap, // map of hugo_gene_symbols or treatment_id's molecularProfileId:string -}; +} /* fields and methods in the class below are ordered based on roughly /* chronological setup concerns, rather than on encapsulation and public API */ @@ -130,7 +138,11 @@ export default class ResultsViewOncoprint extends React.Component{ + this.initFromUrlParams(getBrowserWindow().globalStores.routing.location.query); + }); onMobxPromise(props.store.studyIds, (studyIds:string[])=>{ if (studyIds.length > 1) { @@ -179,7 +191,8 @@ export default class ResultsViewOncoprint extends React.Component[ this.columnMode, this.heatmapTrackGroupsUrlParam, - this.clinicalTracksUrlParam + this.clinicalTracksUrlParam, + this.treatmentsUrlParam ], ()=>{ const newParams = Object.assign({}, getBrowserWindow().globalStores.routing.location.query); @@ -194,6 +207,11 @@ export default class ResultsViewOncoprint extends React.Component{ this.addHeatmapTracks(this.selectedHeatmapProfile, this.heatmapGeneInputValue.toUpperCase().trim().split(/\s+/)); }, + onClickAddTreatmentsToHeatmap:(treatmentIds:string[])=>{ + this.addHeatmapTracks(this.selectedHeatmapProfile, treatmentIds); + }, onClickRemoveHeatmap:action(() => { this.molecularProfileIdToHeatmapTracks.clear(); }), @@ -551,6 +578,8 @@ export default class ResultsViewOncoprint extends React.Component(sampleKeyToSample[key].sampleId)) : @@ -638,28 +667,45 @@ export default class ResultsViewOncoprint extends React.Componentx.trackGroupIndex) - .filter((x:HeatmapTrackGroupRecord)=>!!x.genes.size) - .map((x:HeatmapTrackGroupRecord)=>`${x.molecularProfileId},${x.genes.keys().join(",")}`) - .join(";"); + .filter((x:HeatmapTrackGroupRecord)=>!!x.entities.size) + .map((x:HeatmapTrackGroupRecord)=>`${x.molecularProfileId},${x.entities.keys().join(",")}`) + .join(";"); + } + + // treatments selected iin heatmap are added to the `treatment_list` url param + @computed get treatmentsUrlParam():string { + return _.filter(this.molecularProfileIdToHeatmapTracks.values(), (x:HeatmapTrackGroupRecord)=> x.molecularAlterationType === AlterationTypeConstants.GENERIC_ASSAY) + .map((x:HeatmapTrackGroupRecord)=>`${x.entities.keys().join(";")}`) + .join(";"); + } + + @computed get selectedHeatmapProfileAlterationType():string { + let molecularProfile = this.props.store.molecularProfileIdToMolecularProfile.result[this.selectedHeatmapProfile]; + return molecularProfile.molecularAlterationType; } - private addHeatmapTracks(molecularProfileId:string, genes:string[]) { - let trackGroup = this.molecularProfileIdToHeatmapTracks.get(molecularProfileId); - if (!trackGroup) { - let newTrackGroupIndex = 2; - for (const group of this.molecularProfileIdToHeatmapTracks.values()) { - newTrackGroupIndex = Math.max(newTrackGroupIndex, group.trackGroupIndex + 1); + private addHeatmapTracks(molecularProfileId:string, entities:string[]) { + const profile:MolecularProfile = this.props.store.molecularProfileIdToMolecularProfile.result[molecularProfileId]; + if (profile) { + let trackGroup = this.molecularProfileIdToHeatmapTracks.get(molecularProfileId); + if (!trackGroup) { + let newTrackGroupIndex = 2; + for (const group of this.molecularProfileIdToHeatmapTracks.values()) { + newTrackGroupIndex = Math.max(newTrackGroupIndex, group.trackGroupIndex + 1); + } + + trackGroup = observable({ + trackGroupIndex: newTrackGroupIndex, + molecularProfileId, + molecularAlterationType: profile.molecularAlterationType, + entities: observable.shallowMap({}) + }); + this.molecularProfileIdToHeatmapTracks.set(molecularProfileId, trackGroup); + } + for (const entity of entities) { + trackGroup!.entities.set(entity, true); } - trackGroup = observable({ - trackGroupIndex: newTrackGroupIndex, - molecularProfileId, - genes: observable.shallowMap({}) - }); - } - for (const gene of genes) { - trackGroup!.genes.set(gene, true); } - this.molecularProfileIdToHeatmapTracks.set(molecularProfileId, trackGroup); } private toggleColumnMode() { @@ -796,11 +842,18 @@ export default class ResultsViewOncoprint extends React.Component hmTrack.trackGroupIndex)) + ...(this.heatmapTracks.result.map(hmTrack => hmTrack.trackGroupIndex)), + ...(this.treatmentHeatmapTracks.result.map(hmTrack => hmTrack.trackGroupIndex)) ); } @@ -816,7 +869,6 @@ export default class ResultsViewOncoprint extends React.Component ); } -} \ No newline at end of file +} diff --git a/src/shared/components/oncoprint/TooltipUtils.spec.ts b/src/shared/components/oncoprint/TooltipUtils.spec.ts index eae5ff87cfb..b385b4dc9f2 100644 --- a/src/shared/components/oncoprint/TooltipUtils.spec.ts +++ b/src/shared/components/oncoprint/TooltipUtils.spec.ts @@ -1,126 +1,126 @@ -import {assert} from "chai"; +import { assert } from "chai"; import { getCaseViewElt, makeClinicalTrackTooltip, makeGeneticTrackTooltip, makeGeneticTrackTooltip_getCoverageInformation, makeHeatmapTrackTooltip } from "./TooltipUtils"; -import {GeneticTrackDatum} from "./Oncoprint"; +import { GeneticTrackDatum } from "./Oncoprint"; import { AlterationTypeConstants, AnnotatedExtendedAlteration, AnnotatedMutation } from "../../../pages/resultsView/ResultsViewPageStore"; import $ from "jquery"; -import {MolecularProfile, Mutation} from "../../api/generated/CBioPortalAPI"; -import {getPatientViewUrl, getSampleViewUrl} from "../../api/urls"; +import { MolecularProfile, Mutation } from "../../api/generated/CBioPortalAPI"; +import { getPatientViewUrl, getSampleViewUrl } from "../../api/urls"; -describe("Oncoprint TooltipUtils", ()=>{ - describe("getCaseViewElt", ()=>{ - it("gives empty result for no input", ()=>{ +describe("Oncoprint TooltipUtils", () => { + describe("getCaseViewElt", () => { + it("gives empty result for no input", () => { assert.equal(getCaseViewElt([], false), ""); }); - it("shows case id if single input and no linkout", ()=>{ + it("shows case id if single input and no linkout", () => { assert.equal( - getCaseViewElt([{sample:"sampleID", patient:"patientID", study_id:"studyID"}], false), + getCaseViewElt([{ sample: "sampleID", patient: "patientID", study_id: "studyID" }], false), "sampleID" ); assert.equal( - getCaseViewElt([{patient:"patientID", study_id:"studyID"}], false), + getCaseViewElt([{ patient: "patientID", study_id: "studyID" }], false), "patientID" ); }); - it("shows link if single input and linkout", ()=>{ - let elt = $(getCaseViewElt([{sample:"sampleID", patient:"patientID", study_id:"studyID"}], true)); + it("shows link if single input and linkout", () => { + let elt = $(getCaseViewElt([{ sample: "sampleID", patient: "patientID", study_id: "studyID" }], true)); assert.equal(elt.attr("href"), getSampleViewUrl("studyID", "sampleID")); assert.equal(elt.text(), "sampleID"); - elt = $(getCaseViewElt([{patient:"patientID", study_id:"studyID"}], true)); + elt = $(getCaseViewElt([{ patient: "patientID", study_id: "studyID" }], true)); assert.equal(elt.attr("href"), getPatientViewUrl("studyID", "patientID")); assert.equal(elt.text(), "patientID"); }); - it("shows number if multiple input and no linkout", ()=>{ + it("shows number if multiple input and no linkout", () => { assert.equal( - getCaseViewElt([{sample:"sampleID", patient:"patientID", study_id:"studyID"},{sample:"sampleID2", patient:"patientID2", study_id:"studyID"},{sample:"sampleID3", patient:"patientID3", study_id:"studyID"}], false), + getCaseViewElt([{ sample: "sampleID", patient: "patientID", study_id: "studyID" }, { sample: "sampleID2", patient: "patientID2", study_id: "studyID" }, { sample: "sampleID3", patient: "patientID3", study_id: "studyID" }], false), "3 samples" ); assert.equal( - getCaseViewElt([{patient:"patientID", study_id:"studyID"},{patient:"patientID2", study_id:"studyID"},{patient:"patientID3", study_id:"studyID"}], false), + getCaseViewElt([{ patient: "patientID", study_id: "studyID" }, { patient: "patientID2", study_id: "studyID" }, { patient: "patientID3", study_id: "studyID" }], false), "3 patients" ); }); - it("shows link if multiple input and linkout", ()=>{ - let elt = $(getCaseViewElt([{sample:"sampleID", patient:"patientID", study_id:"studyID"},{sample:"sampleID2", patient:"patientID2", study_id:"studyID"},{sample:"sampleID3", patient:"patientID3", study_id:"studyID"}], true)); - assert.equal(elt.attr("href"), getSampleViewUrl("studyID", "sampleID",[{patientId:"patientID", studyId:"studyID"}, {patientId:"patientID2", studyId:"studyID"}, {patientId:"patientID3", studyId:"studyID"}])); + it("shows link if multiple input and linkout", () => { + let elt = $(getCaseViewElt([{ sample: "sampleID", patient: "patientID", study_id: "studyID" }, { sample: "sampleID2", patient: "patientID2", study_id: "studyID" }, { sample: "sampleID3", patient: "patientID3", study_id: "studyID" }], true)); + assert.equal(elt.attr("href"), getSampleViewUrl("studyID", "sampleID", [{ patientId: "patientID", studyId: "studyID" }, { patientId: "patientID2", studyId: "studyID" }, { patientId: "patientID3", studyId: "studyID" }])); assert.equal(elt.text(), "View these 3 samples"); - elt = $(getCaseViewElt([{patient:"patientID", study_id:"studyID"},{patient:"patientID2", study_id:"studyID"},{patient:"patientID3", study_id:"studyID"}], true)); - assert.equal(elt.attr("href"), getPatientViewUrl("studyID", "patientID",[{patientId:"patientID", studyId:"studyID"}, {patientId:"patientID2", studyId:"studyID"}, {patientId:"patientID3", studyId:"studyID"}])); + elt = $(getCaseViewElt([{ patient: "patientID", study_id: "studyID" }, { patient: "patientID2", study_id: "studyID" }, { patient: "patientID3", study_id: "studyID" }], true)); + assert.equal(elt.attr("href"), getPatientViewUrl("studyID", "patientID", [{ patientId: "patientID", studyId: "studyID" }, { patientId: "patientID2", studyId: "studyID" }, { patientId: "patientID3", studyId: "studyID" }])); assert.equal(elt.text(), "View these 3 patients"); }); }); - describe("makeGeneticTrackTooltip", ()=>{ - let tooltip:(d:any)=>JQuery; - before(()=>{ - tooltip = makeGeneticTrackTooltip(false, ()=>({ - profile:{molecularProfileId:"profile", name:"Profile"} as any as MolecularProfile, - profile2:{molecularProfileId:"profile2", name:"Profile2"} as any as MolecularProfile, - profile3:{molecularProfileId:"profile3", name:"Profile3"} as any as MolecularProfile, + describe("makeGeneticTrackTooltip", () => { + let tooltip: (d: any) => JQuery; + before(() => { + tooltip = makeGeneticTrackTooltip(false, () => ({ + profile: { molecularProfileId: "profile", name: "Profile" } as any as MolecularProfile, + profile2: { molecularProfileId: "profile2", name: "Profile2" } as any as MolecularProfile, + profile3: { molecularProfileId: "profile3", name: "Profile3" } as any as MolecularProfile, })); }); - function makeMutation(props:Partial):AnnotatedExtendedAlteration { + function makeMutation(props: Partial): AnnotatedExtendedAlteration { return { - hugoGeneSymbol:"GENE", - proteinChange:"proteinchange", + hugoGeneSymbol: "GENE", + proteinChange: "proteinchange", molecularProfileAlterationType: AlterationTypeConstants.MUTATION_EXTENDED, ...props } as AnnotatedExtendedAlteration; } - function makeFusion(props:Partial):AnnotatedExtendedAlteration { - return makeMutation({ alterationSubType:"fusion", ...props }); + function makeFusion(props: Partial): AnnotatedExtendedAlteration { + return makeMutation({ alterationSubType: "fusion", ...props }); } - function makeCna(props:Partial):AnnotatedExtendedAlteration { + function makeCna(props: Partial): AnnotatedExtendedAlteration { return { - hugoGeneSymbol:"GENE", + hugoGeneSymbol: "GENE", molecularProfileAlterationType: AlterationTypeConstants.COPY_NUMBER_ALTERATION, ...props } as AnnotatedExtendedAlteration; } - function makeMrna(props:Partial):AnnotatedExtendedAlteration { + function makeMrna(props: Partial): AnnotatedExtendedAlteration { return { - hugoGeneSymbol:"GENE", + hugoGeneSymbol: "GENE", molecularProfileAlterationType: AlterationTypeConstants.MRNA_EXPRESSION, ...props } as AnnotatedExtendedAlteration; } - function makeProt(props:Partial):AnnotatedExtendedAlteration { + function makeProt(props: Partial): AnnotatedExtendedAlteration { return { - hugoGeneSymbol:"GENE", + hugoGeneSymbol: "GENE", molecularProfileAlterationType: AlterationTypeConstants.PROTEIN_LEVEL, ...props } as AnnotatedExtendedAlteration; } - describe("custom driver annotations", ()=>{ - it("should show a binary custom driver icon with descriptive title, if theres a binary custom driver annotation", ()=>{ + describe("custom driver annotations", () => { + it("should show a binary custom driver icon with descriptive title, if theres a binary custom driver annotation", () => { const datum = { - sample: "sample", study_id:"", + sample: "sample", study_id: "", data: [makeMutation({ - driverFilter:"Putative_Driver", + driverFilter: "Putative_Driver", driverFilterAnnotation: "annotation here" })], }; const datum2 = { - sample: "sample", study_id:"", + sample: "sample", study_id: "", data: [makeMutation({ - driverFilter:"Putative_Driver", + driverFilter: "Putative_Driver", driverFilterAnnotation: "annotation here" })], }; const datum3 = { - sample:"sample", study_id:"", - data:[], + sample: "sample", study_id: "", + data: [], }; let tooltipOutput = tooltip([datum]); assert.equal(tooltipOutput.find("img[title='Putative_Driver: annotation here']").length, 1); @@ -138,36 +138,36 @@ describe("Oncoprint TooltipUtils", ()=>{ assert.isTrue(tooltipOutput.html().indexOf("(1)") > -1, "theres a (1) indicating 1 sample has that mutation"); }); - it("should show multiple binary custom driver icons with corresponding titles, if there are multiple annotated mutations", ()=>{ + it("should show multiple binary custom driver icons with corresponding titles, if there are multiple annotated mutations", () => { const datum = { - sample: "sample", study_id:"", + sample: "sample", study_id: "", data: [makeMutation({ - driverFilter:"Putative_Driver", + driverFilter: "Putative_Driver", driverFilterAnnotation: "annotation 1" }), makeMutation({ - driverFilter:"Putative_Driver", + driverFilter: "Putative_Driver", driverFilterAnnotation: "annotation 2" }), makeMutation({ - driverFilter:"Putative_Driver", + driverFilter: "Putative_Driver", driverFilterAnnotation: "3 annotation" })], }; const datum2 = { - sample: "sample", study_id:"", + sample: "sample", study_id: "", data: [makeMutation({ - driverFilter:"Putative_Driver", + driverFilter: "Putative_Driver", driverFilterAnnotation: "annotation 1" }), makeMutation({ - driverFilter:"Putative_Driver", + driverFilter: "Putative_Driver", driverFilterAnnotation: "annotation 2" }), makeMutation({ - driverFilter:"Putative_Driver", + driverFilter: "Putative_Driver", driverFilterAnnotation: "3 annotation" })], }; const datum3 = { - sample: "sample", study_id:"", - data:[], + sample: "sample", study_id: "", + data: [], }; let tooltipOutput = tooltip([datum]); assert.equal(tooltipOutput.find("img[title='Putative_Driver: annotation 1']").length, 1); @@ -191,14 +191,14 @@ describe("Oncoprint TooltipUtils", ()=>{ assert.equal(tooltipOutput.html().match(/\(1\)/g)!.length, 3, "the string (1) appears thrice, one for each mutation which occurs in 1 data"); }); - it("should not show a binary custom driver icon with descriptive title, if theres a binary annotation of non-driver", ()=>{ + it("should not show a binary custom driver icon with descriptive title, if theres a binary annotation of non-driver", () => { const datum = { - sample: "sample", study_id:"", + sample: "sample", study_id: "", data: [makeMutation({ - driverFilter:"Putative_Passenger", + driverFilter: "Putative_Passenger", driverFilterAnnotation: "paosidjp" - }),makeMutation({ - driverFilter:"Unknown", + }), makeMutation({ + driverFilter: "Unknown", driverFilterAnnotation: "asdfas" })], }; @@ -206,23 +206,23 @@ describe("Oncoprint TooltipUtils", ()=>{ assert.equal(tooltipOutput.find("img[alt='driver filter']").length, 0); }); - it("should show a tiers custom driver icon with descriptive title, if theres a tiers custom driver annotation", ()=>{ + it("should show a tiers custom driver icon with descriptive title, if theres a tiers custom driver annotation", () => { const datum = { - sample: "sample", study_id:"", + sample: "sample", study_id: "", data: [makeMutation({ - driverTiersFilter:"tier1", + driverTiersFilter: "tier1", driverTiersFilterAnnotation: "tier1 mutation" })], }; const datum2 = { - sample: "sample", study_id:"", + sample: "sample", study_id: "", data: [makeMutation({ - driverTiersFilter:"tier1", + driverTiersFilter: "tier1", driverTiersFilterAnnotation: "tier1 mutation" })], }; const datum3 = { - sample: "sample", study_id:"", + sample: "sample", study_id: "", data: [], }; let tooltipOutput = tooltip([datum]); @@ -241,35 +241,35 @@ describe("Oncoprint TooltipUtils", ()=>{ assert.isTrue(tooltipOutput.html().indexOf("(1)") > -1, "theres a (1) indicating 1 sample has that mutation"); }); - it("should show multiple tiers icons with corresponding titles, if there are multiple annotated mutations", ()=>{ + it("should show multiple tiers icons with corresponding titles, if there are multiple annotated mutations", () => { const datum = { - sample: "sample", study_id:"", + sample: "sample", study_id: "", data: [makeMutation({ - driverTiersFilter:"tier2", + driverTiersFilter: "tier2", driverTiersFilterAnnotation: "tier2 mutation" - }),makeMutation({ - driverTiersFilter:"tier1", + }), makeMutation({ + driverTiersFilter: "tier1", driverTiersFilterAnnotation: "tier1 mutation" - }),makeMutation({ - driverTiersFilter:"tier4", + }), makeMutation({ + driverTiersFilter: "tier4", driverTiersFilterAnnotation: "mutation tier4" })], }; const datum2 = { - sample: "sample", study_id:"", + sample: "sample", study_id: "", data: [makeMutation({ - driverTiersFilter:"tier2", + driverTiersFilter: "tier2", driverTiersFilterAnnotation: "tier2 mutation" - }),makeMutation({ - driverTiersFilter:"tier1", + }), makeMutation({ + driverTiersFilter: "tier1", driverTiersFilterAnnotation: "tier1 mutation" - }),makeMutation({ - driverTiersFilter:"tier4", + }), makeMutation({ + driverTiersFilter: "tier4", driverTiersFilterAnnotation: "mutation tier4" })] }; const datum3 = { - sample: "sample", study_id:"", + sample: "sample", study_id: "", data: [] }; let tooltipOutput = tooltip([datum]); @@ -294,32 +294,32 @@ describe("Oncoprint TooltipUtils", ()=>{ assert.equal(tooltipOutput.html().match(/\(1\)/g)!.length, 3, "the string (1) appears thrice, one for each mutation which occurs in 1 data"); }); - it("should show both binary and tiers custom driver icons, with descriptive titles, if there are both annotations", ()=>{ + it("should show both binary and tiers custom driver icons, with descriptive titles, if there are both annotations", () => { const datum = { - sample: "sample", study_id:"", + sample: "sample", study_id: "", data: [makeMutation({ - driverFilter:"Putative_Driver", + driverFilter: "Putative_Driver", driverFilterAnnotation: "annotation 1" }), makeMutation({ - driverFilter:"Putative_Driver", + driverFilter: "Putative_Driver", driverFilterAnnotation: "annotation 2" }), makeMutation({ - driverFilter:"Putative_Driver", + driverFilter: "Putative_Driver", driverFilterAnnotation: "3 annotation" - }),makeMutation({ - driverFilter:"Putative_Passenger", + }), makeMutation({ + driverFilter: "Putative_Passenger", driverFilterAnnotation: "paosidjp" - }),makeMutation({ - driverFilter:"Unknown", + }), makeMutation({ + driverFilter: "Unknown", driverFilterAnnotation: "asdfas" - }),makeMutation({ - driverTiersFilter:"tier2", + }), makeMutation({ + driverTiersFilter: "tier2", driverTiersFilterAnnotation: "tier2 mutation" - }),makeMutation({ - driverTiersFilter:"tier1", + }), makeMutation({ + driverTiersFilter: "tier1", driverTiersFilterAnnotation: "tier1 mutation" - }),makeMutation({ - driverTiersFilter:"tier4", + }), makeMutation({ + driverTiersFilter: "tier4", driverTiersFilterAnnotation: "mutation tier4" })] }; @@ -334,35 +334,35 @@ describe("Oncoprint TooltipUtils", ()=>{ assert.equal(tooltipOutput.find("img[alt='driver filter']").length, 3, "should be three binary icons"); }); - it("should show neither icon if theres no custom driver annotations", ()=>{ + it("should show neither icon if theres no custom driver annotations", () => { const datum = { - sample: "sample", study_id:"", + sample: "sample", study_id: "", data: [makeMutation({ - driverFilter:"Putative_Passenger", + driverFilter: "Putative_Passenger", driverFilterAnnotation: "paosidjp" - }),makeMutation({ - driverFilter:"Unknown", + }), makeMutation({ + driverFilter: "Unknown", driverFilterAnnotation: "asdfas" - }),makeMutation({}), makeMutation({})] + }), makeMutation({}), makeMutation({})] }; const tooltipOutput = tooltip([datum]); assert.equal(tooltipOutput.find("img[alt='driver filter']").length, 0, "should be no binary icons"); assert.equal(tooltipOutput.find("img[alt='driver tiers filter']").length, 0, "should be no tiers icons"); }); }); - describe("germline mutations", ()=>{ - it("should show Germline next to every germline mutation, not next to others", () =>{ + describe("germline mutations", () => { + it("should show Germline next to every germline mutation, not next to others", () => { const datum = { sample: "sample", study_id: "", - data: [makeMutation({ mutationStatus: "germline", proteinChange:"mutation1"}), makeMutation({mutationStatus:"germline", proteinChange:"mutation2"}), makeMutation({})], + data: [makeMutation({ mutationStatus: "germline", proteinChange: "mutation1" }), makeMutation({ mutationStatus: "germline", proteinChange: "mutation2" }), makeMutation({})], }; const datum2 = { sample: "sample", study_id: "", - data: [makeMutation({ mutationStatus: "germline", proteinChange:"mutation1"}), makeMutation({proteinChange:"mutation2"})] + data: [makeMutation({ mutationStatus: "germline", proteinChange: "mutation1" }), makeMutation({ proteinChange: "mutation2" })] }; const datum3 = { sample: "sample", study_id: "", - data: [makeMutation({ proteinChange:"mutation1"}), makeMutation({mutationStatus:"germline", proteinChange:"mutation2"})] + data: [makeMutation({ proteinChange: "mutation1" }), makeMutation({ mutationStatus: "germline", proteinChange: "mutation2" })] }; let tooltipOutput = tooltip([datum]); assert.equal(tooltipOutput.html().match(/Germline/g)!.length, 2); @@ -388,14 +388,14 @@ describe("Oncoprint TooltipUtils", ()=>{ assert.equal(tooltipOutput.html().match(/mutation2/g)!.length, 2, "since one is germline and one is not, should appear twice in tooltip"); }); }); - describe("profiled and not profiled", ()=>{ - it("should say 'Not profiled' if 'profiled_in' is empty and 'not_profiled_in' is not", ()=>{ + describe("profiled and not profiled", () => { + it("should say 'Not profiled' if 'profiled_in' is empty and 'not_profiled_in' is not", () => { const datum = { - sample: "sample", study_id:"", + sample: "sample", study_id: "", data: [] as AnnotatedExtendedAlteration[], - na:true, - profiled_in:[], - not_profiled_in:[{molecularProfileId:"profile"}] + na: true, + profiled_in: [], + not_profiled_in: [{ molecularProfileId: "profile" }] }; let tooltipOutput = tooltip([datum]); assert.isTrue(tooltipOutput.html().indexOf("Not profiled in selected molecular profiles.") > -1); @@ -407,13 +407,13 @@ describe("Oncoprint TooltipUtils", ()=>{ tooltipOutput = tooltip([datum, datum, datum]); assert.isTrue(tooltipOutput.html().indexOf("Not profiled in selected molecular profiles. (3)") > -1); }); - it("should say 'profiled' if 'not_profiled_in' is empty and 'profiled_in' is not", ()=>{ + it("should say 'profiled' if 'not_profiled_in' is empty and 'profiled_in' is not", () => { const datum = { - sample: "sample", study_id:"", + sample: "sample", study_id: "", data: [] as AnnotatedExtendedAlteration[], - na:true, - profiled_in:[{molecularProfileId:"profile", genePanelId:"panel"}], - not_profiled_in:[] + na: true, + profiled_in: [{ molecularProfileId: "profile", genePanelId: "panel" }], + not_profiled_in: [] }; let tooltipOutput = tooltip([datum]); assert.isTrue(tooltipOutput.html().indexOf("Profiled in all selected molecular profiles.") > -1); @@ -425,41 +425,41 @@ describe("Oncoprint TooltipUtils", ()=>{ tooltipOutput = tooltip([datum, datum, datum]); assert.isTrue(tooltipOutput.html().indexOf("Profiled in all selected molecular profiles. (3)") > -1); }); - it("should give the correct spread of 'Not profiled' and 'Profiled' over multiple data", ()=>{ + it("should give the correct spread of 'Not profiled' and 'Profiled' over multiple data", () => { const notProfiled = { - sample: "sample", study_id:"", + sample: "sample", study_id: "", data: [] as AnnotatedExtendedAlteration[], - na:true, - profiled_in:[], - not_profiled_in:[{molecularProfileId:"profile"}, {molecularProfileId:"profile2"}, {molecularProfileId:"profile3"}] + na: true, + profiled_in: [], + not_profiled_in: [{ molecularProfileId: "profile" }, { molecularProfileId: "profile2" }, { molecularProfileId: "profile3" }] }; const profiled = { - sample: "sample", study_id:"", + sample: "sample", study_id: "", data: [] as AnnotatedExtendedAlteration[], - na:true, - profiled_in:[{molecularProfileId:"profile"}, {molecularProfileId:"profile2"}, {molecularProfileId:"profile3"}], - not_profiled_in:[] + na: true, + profiled_in: [{ molecularProfileId: "profile" }, { molecularProfileId: "profile2" }, { molecularProfileId: "profile3" }], + not_profiled_in: [] }; const datum1 = { - sample: "sample", study_id:"", + sample: "sample", study_id: "", data: [] as AnnotatedExtendedAlteration[], - na:true, - profiled_in:[{molecularProfileId:"profile"}], - not_profiled_in:[{molecularProfileId:"profile2"}, {molecularProfileId:"profile3"}] + na: true, + profiled_in: [{ molecularProfileId: "profile" }], + not_profiled_in: [{ molecularProfileId: "profile2" }, { molecularProfileId: "profile3" }] }; const datum2 = { - sample: "sample", study_id:"", + sample: "sample", study_id: "", data: [] as AnnotatedExtendedAlteration[], - na:true, - profiled_in:[{molecularProfileId:"profile"}, {molecularProfileId:"profile2"}], - not_profiled_in:[{molecularProfileId:"profile3"}] + na: true, + profiled_in: [{ molecularProfileId: "profile" }, { molecularProfileId: "profile2" }], + not_profiled_in: [{ molecularProfileId: "profile3" }] }; const datum3 = { - sample: "sample", study_id:"", + sample: "sample", study_id: "", data: [] as AnnotatedExtendedAlteration[], - na:true, - profiled_in:[{molecularProfileId:"profile2"},{molecularProfileId:"profile3"}], - not_profiled_in:[{molecularProfileId:"profile"}] + na: true, + profiled_in: [{ molecularProfileId: "profile2" }, { molecularProfileId: "profile3" }], + not_profiled_in: [{ molecularProfileId: "profile" }] }; let tooltipOutput = tooltip([notProfiled, profiled, notProfiled]); @@ -492,28 +492,28 @@ describe("Oncoprint TooltipUtils", ()=>{ assert.isTrue(tooltipOutput.html().indexOf("Not profiled in: Profile2 (2), Profile3 (3), Profile (2)") > -1, "all the counts are correct (order is arbitrary)"); }); }); - describe("gene panel information", ()=>{ - it("should show the correct gene panel entries in the tooltip, single and multiple data", ()=>{ + describe("gene panel information", () => { + it("should show the correct gene panel entries in the tooltip, single and multiple data", () => { const datum1 = { - sample: "sample", study_id:"", + sample: "sample", study_id: "", data: [] as AnnotatedExtendedAlteration[], - na:true, - profiled_in:[{molecularProfileId:"profile", genePanelId:"panel1"}, {molecularProfileId:"profile", genePanelId:"panel2"}, {molecularProfileId:"profile2", genePanelId:"panel3"}], - not_profiled_in:[] + na: true, + profiled_in: [{ molecularProfileId: "profile", genePanelId: "panel1" }, { molecularProfileId: "profile", genePanelId: "panel2" }, { molecularProfileId: "profile2", genePanelId: "panel3" }], + not_profiled_in: [] }; const datum2 = { - sample: "sample", study_id:"", + sample: "sample", study_id: "", data: [] as AnnotatedExtendedAlteration[], - na:true, - profiled_in:[], - not_profiled_in:[{molecularProfileId:"profile", genePanelId:"panel1"}, {molecularProfileId:"profile", genePanelId:"panel2"}, {molecularProfileId:"profile2", genePanelId:"panel3"}] + na: true, + profiled_in: [], + not_profiled_in: [{ molecularProfileId: "profile", genePanelId: "panel1" }, { molecularProfileId: "profile", genePanelId: "panel2" }, { molecularProfileId: "profile2", genePanelId: "panel3" }] }; const datum3 = { - sample: "sample", study_id:"", + sample: "sample", study_id: "", data: [] as AnnotatedExtendedAlteration[], - na:true, - profiled_in:[{molecularProfileId:"profile", genePanelId:"panel1"}, {molecularProfileId:"profile", genePanelId:"panel2"}], - not_profiled_in:[{molecularProfileId:"profile2", genePanelId:"panel3"}] + na: true, + profiled_in: [{ molecularProfileId: "profile", genePanelId: "panel1" }, { molecularProfileId: "profile", genePanelId: "panel2" }], + not_profiled_in: [{ molecularProfileId: "profile2", genePanelId: "panel3" }] }; let tooltipOutput = tooltip([datum1]); @@ -567,56 +567,56 @@ describe("Oncoprint TooltipUtils", ()=>{ assert.notEqual(tooltipOutput.find('a:contains("panel3")').first().css("color"), "red"); }); }); - describe("genetic alterations", ()=>{ - let datum:any; - let emptyDatum:any; - let tooltipOutput:JQuery; - beforeEach(()=>{ + describe("genetic alterations", () => { + let datum: any; + let emptyDatum: any; + let tooltipOutput: JQuery; + beforeEach(() => { datum = { - sample: "sample", study_id:"", + sample: "sample", study_id: "", data: [] as AnnotatedExtendedAlteration[], - na:false, - profiled_in:[], - not_profiled_in:[] + na: false, + profiled_in: [], + not_profiled_in: [] }; emptyDatum = { - sample: "sample", study_id:"", + sample: "sample", study_id: "", data: [] as AnnotatedExtendedAlteration[], - na:false, - profiled_in:[], - not_profiled_in:[] + na: false, + profiled_in: [], + not_profiled_in: [] }; }); - it("single genetic alteration in single case - mutation", ()=>{ - datum.data = [makeMutation({proteinChange:"PC1"})]; + it("single genetic alteration in single case - mutation", () => { + datum.data = [makeMutation({ proteinChange: "PC1" })]; tooltipOutput = tooltip([datum]); assert.equal(tooltipOutput.text().match(/Mutation:/g)!.length, 1); assert.equal(tooltipOutput.text().match(/GENE PC1/g)!.length, 1); assert.equal(tooltipOutput.text().match(/\(\d+\)/g), null, "no number indicator for single case"); - datum.data = [makeMutation({proteinChange:"PC1"}), makeMutation({proteinChange:"PC1"})]; + datum.data = [makeMutation({ proteinChange: "PC1" }), makeMutation({ proteinChange: "PC1" })]; tooltipOutput = tooltip([datum]); assert.equal(tooltipOutput.text().match(/Mutation:/g)!.length, 1); assert.equal(tooltipOutput.text().match(/GENE PC1/g)!.length, 1); assert.equal(tooltipOutput.text().match(/\(\d+\)/g), null, "no number indicator for single case"); }); - it("single genetic alteration in single case - fusion", ()=>{ - datum.data = [makeFusion({proteinChange:"PC1"})]; + it("single genetic alteration in single case - fusion", () => { + datum.data = [makeFusion({ proteinChange: "PC1" })]; tooltipOutput = tooltip([datum]); assert.equal(tooltipOutput.text().match(/Fusion:/g)!.length, 1); assert.equal(tooltipOutput.text().match(/GENE PC1/g)!.length, 1); assert.equal(tooltipOutput.text().match(/\(\d+\)/g), null, "no number indicator for single case"); - datum.data = [makeFusion({proteinChange:"PC1"}), makeFusion({proteinChange:"PC1"})]; + datum.data = [makeFusion({ proteinChange: "PC1" }), makeFusion({ proteinChange: "PC1" })]; tooltipOutput = tooltip([datum]); assert.equal(tooltipOutput.text().match(/Fusion:/g)!.length, 1); assert.equal(tooltipOutput.text().match(/GENE PC1/g)!.length, 1); assert.equal(tooltipOutput.text().match(/\(\d+\)/g), null, "no number indicator for single case"); }); - it("single genetic alteration in single case - cna", ()=>{ - const disp_cna:{[integerCN:string]:string} = {'-2': 'HOMODELETED', '-1': 'HETLOSS', '1': 'GAIN', '2': 'AMPLIFIED'}; - for (let i=-2; i<=2; i++) { - datum.data = [makeCna({value:i})]; + it("single genetic alteration in single case - cna", () => { + const disp_cna: { [integerCN: string]: string } = { '-2': 'HOMODELETED', '-1': 'HETLOSS', '1': 'GAIN', '2': 'AMPLIFIED' }; + for (let i = -2; i <= 2; i++) { + datum.data = [makeCna({ value: i })]; tooltipOutput = tooltip([datum]); if (i === 0) { @@ -628,7 +628,7 @@ describe("Oncoprint TooltipUtils", ()=>{ assert.equal(tooltipOutput.text().match(/\(\d+\)/g), null, "no number indicator for single case"); } - datum.data = [makeCna({value:i}), makeCna({value:i})]; + datum.data = [makeCna({ value: i }), makeCna({ value: i })]; tooltipOutput = tooltip([datum]); if (i === 0) { @@ -691,8 +691,8 @@ describe("Oncoprint TooltipUtils", ()=>{ assert.equal(tooltipOutput.text().match(/GENE LOW/g)!.length, 1); assert.equal(tooltipOutput.text().match(/\(\d+\)/g), null, "no number indicator for single case"); }); - it("single genetic alteration across multiple cases - mutation", ()=>{ - datum.data = [makeMutation({proteinChange:"PC1"})]; + it("single genetic alteration across multiple cases - mutation", () => { + datum.data = [makeMutation({ proteinChange: "PC1" })]; tooltipOutput = tooltip([datum, datum, datum]); assert.equal(tooltipOutput.text().match(/Mutation:/g)!.length, 1); assert.equal(tooltipOutput.text().match(/GENE PC1\xa0\(3\)/g)!.length, 1); @@ -701,8 +701,8 @@ describe("Oncoprint TooltipUtils", ()=>{ assert.equal(tooltipOutput.text().match(/Mutation:/g)!.length, 1); assert.equal(tooltipOutput.text().match(/GENE PC1\xa0\(1\)/g)!.length, 1); }); - it("single genetic alteration across multiple cases - fusion", ()=>{ - datum.data = [makeFusion({proteinChange:"PC1"})]; + it("single genetic alteration across multiple cases - fusion", () => { + datum.data = [makeFusion({ proteinChange: "PC1" })]; tooltipOutput = tooltip([datum, datum, datum]); assert.equal(tooltipOutput.text().match(/Fusion:/g)!.length, 1); assert.equal(tooltipOutput.text().match(/GENE PC1\xa0\(3\)/g)!.length, 1); @@ -711,10 +711,10 @@ describe("Oncoprint TooltipUtils", ()=>{ assert.equal(tooltipOutput.text().match(/Fusion:/g)!.length, 1); assert.equal(tooltipOutput.text().match(/GENE PC1\xa0\(1\)/g)!.length, 1); }); - it("single genetic alteration across multiple cases - cna", ()=>{ - const disp_cna:{[integerCN:string]:string} = {'-2': 'HOMODELETED', '-1': 'HETLOSS', '1': 'GAIN', '2': 'AMPLIFIED'}; - for (let i=-2; i<=2; i++) { - datum.data = [makeCna({value:i})]; + it("single genetic alteration across multiple cases - cna", () => { + const disp_cna: { [integerCN: string]: string } = { '-2': 'HOMODELETED', '-1': 'HETLOSS', '1': 'GAIN', '2': 'AMPLIFIED' }; + for (let i = -2; i <= 2; i++) { + datum.data = [makeCna({ value: i })]; tooltipOutput = tooltip([emptyDatum, datum, datum, datum, datum]); if (i === 0) { @@ -752,24 +752,24 @@ describe("Oncoprint TooltipUtils", ()=>{ assert.equal(tooltipOutput.text().match(/PROT:/g)!.length, 1); assert.equal(tooltipOutput.text().match(/GENE LOW\xa0\(5\)/g)!.length, 1); }); - it("multiple alterations of same type in single case - mutation", ()=>{ - datum.data = [makeMutation({proteinChange:"PC1"}), makeMutation({proteinChange:"PC2"})]; + it("multiple alterations of same type in single case - mutation", () => { + datum.data = [makeMutation({ proteinChange: "PC1" }), makeMutation({ proteinChange: "PC2" })]; tooltipOutput = tooltip([datum]); assert.equal(tooltipOutput.text().match(/Mutation:/g)!.length, 1); assert.equal(tooltipOutput.text().match(/GENE PC1/g)!.length, 1); assert.equal(tooltipOutput.text().match(/GENE PC2/g)!.length, 1); assert.equal(tooltipOutput.text().match(/\(\d+\)/g), null, "no number indicator for single case"); }); - it("multiple alterations of same type in single case - fusion", ()=>{ - datum.data = [makeFusion({proteinChange:"PC1"}), makeFusion({proteinChange:"PC2"})]; + it("multiple alterations of same type in single case - fusion", () => { + datum.data = [makeFusion({ proteinChange: "PC1" }), makeFusion({ proteinChange: "PC2" })]; tooltipOutput = tooltip([datum]); assert.equal(tooltipOutput.text().match(/Fusion:/g)!.length, 1); assert.equal(tooltipOutput.text().match(/GENE PC1/g)!.length, 1); assert.equal(tooltipOutput.text().match(/GENE PC2/g)!.length, 1); assert.equal(tooltipOutput.text().match(/\(\d+\)/g), null, "no number indicator for single case"); }); - it("multiple alterations of same type in single case - cna", ()=>{ - datum.data = [makeCna({value:-2}), makeCna({value:-1}), makeCna({value:1})]; + it("multiple alterations of same type in single case - cna", () => { + datum.data = [makeCna({ value: -2 }), makeCna({ value: -1 }), makeCna({ value: 1 })]; tooltipOutput = tooltip([datum]); assert.equal(tooltipOutput.text().match(/Copy Number Alteration:/g)!.length, 1); assert.equal(tooltipOutput.text().match(/GENE HOMODELETED/g)!.length, 1); @@ -793,8 +793,8 @@ describe("Oncoprint TooltipUtils", ()=>{ assert.equal(tooltipOutput.text().match(/GENE LOW/g)!.length, 1); assert.equal(tooltipOutput.text().match(/\(\d+\)/g), null, "no number indicator for single case"); }); - it("multiple alterations of same type across multiple cases - mutation", ()=>{ - datum.data = [makeMutation({proteinChange:"PC1"}), makeMutation({proteinChange:"PC2"})]; + it("multiple alterations of same type across multiple cases - mutation", () => { + datum.data = [makeMutation({ proteinChange: "PC1" }), makeMutation({ proteinChange: "PC2" })]; tooltipOutput = tooltip([datum, datum, datum]); assert.equal(tooltipOutput.text().match(/Mutation:/g)!.length, 1); assert.equal(tooltipOutput.text().match(/GENE PC1\xa0\(3\)/g)!.length, 1); @@ -805,8 +805,8 @@ describe("Oncoprint TooltipUtils", ()=>{ assert.equal(tooltipOutput.text().match(/GENE PC1\xa0\(1\)/g)!.length, 1); assert.equal(tooltipOutput.text().match(/GENE PC2\xa0\(1\)/g)!.length, 1); }); - it("multiple alterations of same type across multiple cases - fusion", ()=>{ - datum.data = [makeFusion({proteinChange:"PC1"}), makeFusion({proteinChange:"PC2"})]; + it("multiple alterations of same type across multiple cases - fusion", () => { + datum.data = [makeFusion({ proteinChange: "PC1" }), makeFusion({ proteinChange: "PC2" })]; tooltipOutput = tooltip([datum, datum, datum, datum]); assert.equal(tooltipOutput.text().match(/Fusion:/g)!.length, 1); assert.equal(tooltipOutput.text().match(/GENE PC1\xa0\(4\)/g)!.length, 1); @@ -817,8 +817,8 @@ describe("Oncoprint TooltipUtils", ()=>{ assert.equal(tooltipOutput.text().match(/GENE PC1\xa0\(2\)/g)!.length, 1); assert.equal(tooltipOutput.text().match(/GENE PC2\xa0\(2\)/g)!.length, 1); }); - it("multiple alterations of same type across multiple cases - cna", ()=>{ - datum.data = [makeCna({value:-2}), makeCna({value:-1}), makeCna({value:1})]; + it("multiple alterations of same type across multiple cases - cna", () => { + datum.data = [makeCna({ value: -2 }), makeCna({ value: -1 }), makeCna({ value: 1 })]; tooltipOutput = tooltip([datum, datum, datum]); assert.equal(tooltipOutput.text().match(/Copy Number Alteration:/g)!.length, 1); assert.equal(tooltipOutput.text().match(/GENE HOMODELETED\xa0\(3\)/g)!.length, 1); @@ -855,7 +855,7 @@ describe("Oncoprint TooltipUtils", ()=>{ assert.equal(tooltipOutput.text().match(/GENE HIGH\xa0\(1\)/g)!.length, 1); assert.equal(tooltipOutput.text().match(/GENE LOW\xa0\(2\)/g)!.length, 1); }); - it("multiple alterations of different types in single case", ()=>{ + it("multiple alterations of different types in single case", () => { datum.data = [ makeMutation({proteinChange:"PC1"}), makeMutation({proteinChange:"PC2"}), makeFusion({proteinChange:"fusion1"}), makeFusion({proteinChange:"fusion2"}), @@ -883,7 +883,7 @@ describe("Oncoprint TooltipUtils", ()=>{ assert.equal(tooltipOutput.text().match(/\(\d+\)/g), null, "no number indicator for single case"); }); - it("multiple alterations of different types across multiple cases", ()=>{ + it("multiple alterations of different types across multiple cases", () => { datum.data = [ makeMutation({proteinChange:"PC1"}), makeMutation({proteinChange:"PC2"}), makeFusion({proteinChange:"fusion1"}), makeFusion({proteinChange:"fusion2"}), @@ -920,12 +920,12 @@ describe("Oncoprint TooltipUtils", ()=>{ }); }); }); - describe("makeClinicalTrackTooltip", ()=>{ - describe("category track tooltip", ()=>{ - let trackLabel:string; - let trackSpec:any; - let tooltip:(dataUnderMouse:any[])=>JQuery; - before(()=>{ + describe("makeClinicalTrackTooltip", () => { + describe("category track tooltip", () => { + let trackLabel: string; + let trackSpec: any; + let tooltip: (dataUnderMouse: any[]) => JQuery; + before(() => { trackLabel = "label1234"; trackSpec = { key: "", @@ -937,115 +937,146 @@ describe("Oncoprint TooltipUtils", ()=>{ tooltip = makeClinicalTrackTooltip(trackSpec, false); }); - it("should show the given sample id", ()=>{ - const sampleTooltipResult = tooltip([{ attr_val_counts: {"a":1}, attr_val:"a", sample:"sampleID" }]); - assert.isTrue(sampleTooltipResult.html().indexOf("sampleID") > -1 ); + it("should show the given sample id", () => { + const sampleTooltipResult = tooltip([{ attr_val_counts: { "a": 1 }, attr_val: "a", sample: "sampleID" }]); + assert.isTrue(sampleTooltipResult.html().indexOf("sampleID") > -1); }); - it("should show the given patient id", ()=>{ - const patientTooltipResult = tooltip([{ attr_val_counts: {"a":1}, attr_val:"a", patient:"patientID" }]); - assert.isTrue(patientTooltipResult.html().indexOf("patientID") > -1 ); + it("should show the given patient id", () => { + const patientTooltipResult = tooltip([{ attr_val_counts: { "a": 1 }, attr_val: "a", patient: "patientID" }]); + assert.isTrue(patientTooltipResult.html().indexOf("patientID") > -1); }); - it("should show the correct output for a single value", ()=>{ - const tooltipResult = tooltip([{ attr_val_counts: {"a":1}, attr_val:"a", sample:"sampleID" }]); + it("should show the correct output for a single value", () => { + const tooltipResult = tooltip([{ attr_val_counts: { "a": 1 }, attr_val: "a", sample: "sampleID" }]); assert.isTrue(tooltipResult.html().indexOf(`label1234: a`) > -1); }); - it("should show the correct output for multiple values", ()=>{ - const tooltipResult = tooltip([{ attr_val_counts: {"a":1, "b":3}, attr_val:"a", sample:"sampleID" }]); + it("should show the correct output for multiple values", () => { + const tooltipResult = tooltip([{ attr_val_counts: { "a": 1, "b": 3 }, attr_val: "a", sample: "sampleID" }]); assert.isTrue(tooltipResult.html().indexOf(`label1234:
a: 1 sample
b: 3 samples`) > -1); }); - it("should show the correct output for multiple data, single value", ()=>{ + it("should show the correct output for multiple data, single value", () => { const tooltipResult = tooltip([ - { attr_val_counts: {"a":1}, attr_val:"a", patient:"patientID" }, - { attr_val_counts: {"a":1}, attr_val:"a", patient:"patientID" }, - { attr_val_counts: {"a":2}, attr_val:"a", patient:"patientID" } + { attr_val_counts: { "a": 1 }, attr_val: "a", patient: "patientID" }, + { attr_val_counts: { "a": 1 }, attr_val: "a", patient: "patientID" }, + { attr_val_counts: { "a": 2 }, attr_val: "a", patient: "patientID" } ]); assert.isTrue(tooltipResult.html().indexOf(`label1234: a (4 samples)
`) > -1); }); - it("should show the correct output for multiple data, multiple values", ()=>{ + it("should show the correct output for multiple data, multiple values", () => { const tooltip = makeClinicalTrackTooltip(trackSpec, false); const tooltipResult = tooltip([ - { attr_val_counts: {"a":1, "b":5}, attr_val:"a", patient:"patientID" }, - { attr_val_counts: {"a":1}, attr_val:"a", patient:"patientID" }, - { attr_val_counts: {"a":2, "b":1, "c":1}, attr_val:"a", patient:"patientID" } + { attr_val_counts: { "a": 1, "b": 5 }, attr_val: "a", patient: "patientID" }, + { attr_val_counts: { "a": 1 }, attr_val: "a", patient: "patientID" }, + { attr_val_counts: { "a": 2, "b": 1, "c": 1 }, attr_val: "a", patient: "patientID" } ]); assert.isTrue(tooltipResult.html().indexOf( `label1234:
a: 4 samples
b: 6 samples
c: 1 sample` ) > -1); }); }); - describe("number track tooltip", ()=>{ - let trackSpec:any; - let tooltip:(dataUnderMouse:any[])=>JQuery; - before(()=>{ + describe("number track tooltip", () => { + let trackSpec: any; + let tooltip: (dataUnderMouse: any[]) => JQuery; + before(() => { trackSpec = { key: "", label: "", description: "", data: [], datatype: "number" as "number", - numberRange:[0,0] as [number, number], - numberLogScale:false + numberRange: [0, 0] as [number, number], + numberLogScale: false }; tooltip = makeClinicalTrackTooltip(trackSpec, false); }); - it("should show numerical data rounded to 2 decimal digits", ()=>{ + it("should show numerical data rounded to 2 decimal digits", () => { // one data - let tooltipResult = tooltip([{ attr_val_counts: {"0.13500013531":1}, attr_val:"0.13500013531", sample:"sampleID" }]); + let tooltipResult = tooltip([{ attr_val_counts: { "0.13500013531": 1 }, attr_val: "0.13500013531", sample: "sampleID" }]); assert.isTrue(tooltipResult.html().indexOf("0.14") > -1, "correct result with no integer part"); - tooltipResult = tooltip([{ attr_val_counts: {"6.100032":1}, attr_val:"6.100032", sample:"sampleID" }]); + tooltipResult = tooltip([{ attr_val_counts: { "6.100032": 1 }, attr_val: "6.100032", sample: "sampleID" }]); assert.isTrue(tooltipResult.html().indexOf("6.10") > -1, "correct result with integer part"); - tooltipResult = tooltip([{ attr_val_counts: {"0":1}, attr_val:"0", sample:"sampleID" }]); + tooltipResult = tooltip([{ attr_val_counts: { "0": 1 }, attr_val: "0", sample: "sampleID" }]); assert.isTrue(tooltipResult.html().indexOf("0") > -1, "correct result for zero") - tooltipResult = tooltip([{ attr_val_counts: {"-0.13500013531":1}, attr_val:"-0.13500013531", sample:"sampleID" }]); + tooltipResult = tooltip([{ attr_val_counts: { "-0.13500013531": 1 }, attr_val: "-0.13500013531", sample: "sampleID" }]); assert.isTrue(tooltipResult.html().indexOf("-0.14") > -1, "correct result with no integer part, negative"); - tooltipResult = tooltip([{ attr_val_counts: {"-6.100032":1}, attr_val:"-6.100032", sample:"sampleID" }]); + tooltipResult = tooltip([{ attr_val_counts: { "-6.100032": 1 }, attr_val: "-6.100032", sample: "sampleID" }]); assert.isTrue(tooltipResult.html().indexOf("-6.10") > -1, "correct result with integer part, negative"); // multiple data // more than one data - tooltipResult = tooltip([{ attr_val_counts:{"0.13500013531":1}, sample:"sampleID"}, { attr_val_counts:{"0.23500013531":1}, sample:"sampleID"}, { attr_val_counts:{"0.33500013531":1}, sample:"sampleID"}]); + tooltipResult = tooltip([{ attr_val_counts: { "0.13500013531": 1 }, sample: "sampleID" }, { attr_val_counts: { "0.23500013531": 1 }, sample: "sampleID" }, { attr_val_counts: { "0.33500013531": 1 }, sample: "sampleID" }]); assert.isTrue(tooltipResult.html().indexOf("0.24 (average of 3 values)") > -1, "multiple - correct result with no integer part"); - tooltipResult = tooltip([{ attr_val_counts:{"6.100032":1}, sample:"sampleID" }, { attr_val_counts:{"8.100032":1}, sample:"sampleID" }]); + tooltipResult = tooltip([{ attr_val_counts: { "6.100032": 1 }, sample: "sampleID" }, { attr_val_counts: { "8.100032": 1 }, sample: "sampleID" }]); assert.isTrue(tooltipResult.html().indexOf("7.10 (average of 2 values)") > -1, "multiple - correct result with integer part"); - tooltipResult = tooltip([{ attr_val_counts: {"0":1}, sample:"sampleID" }, { attr_val_counts: {"0":1}, sample:"sampleID" }, { attr_val_counts: {"0":1}, sample:"sampleID" }, { attr_val_counts: {"0":1}, sample:"sampleID" }]); + tooltipResult = tooltip([{ attr_val_counts: { "0": 1 }, sample: "sampleID" }, { attr_val_counts: { "0": 1 }, sample: "sampleID" }, { attr_val_counts: { "0": 1 }, sample: "sampleID" }, { attr_val_counts: { "0": 1 }, sample: "sampleID" }]); assert.isTrue(tooltipResult.html().indexOf("0 (average of 4 values)") > -1, "multiple - correct result for zero") - tooltipResult = tooltip([{ attr_val_counts:{"-0.03500013531":2, "-0.23500013531":2}, sample:"sampleID" }]); + tooltipResult = tooltip([{ attr_val_counts: { "-0.03500013531": 2, "-0.23500013531": 2 }, sample: "sampleID" }]); assert.isTrue(tooltipResult.html().indexOf("-0.14 (average of 4 values)") > -1, "correct result with no integer part, negative"); - tooltipResult = tooltip([{ attr_val_counts:{"-5.100032":1}, sample:"sampleID" }, { attr_val_counts:{"-2.100032":2}, sample:"sampleID" }]); + tooltipResult = tooltip([{ attr_val_counts: { "-5.100032": 1 }, sample: "sampleID" }, { attr_val_counts: { "-2.100032": 2 }, sample: "sampleID" }]); assert.isTrue(tooltipResult.html().indexOf("-3.10 (average of 3 values)") > -1, "correct result with integer part, negative"); }); }); }); - describe("makeHeatmapTrackTooltip", ()=>{ - it("should show data rounded to 2 decimal digits", ()=>{ - const tooltip = makeHeatmapTrackTooltip("MRNA_EXPRESSION", false); + describe("makeHeatmapTrackTooltip", () => { + + const tooltip = makeHeatmapTrackTooltip("MRNA_EXPRESSION", false); + + it("should show data rounded to 2 decimal digits", () => { // one data - let tooltipResult = tooltip([{ profile_data:0.13500013531, sample:"sampleID" }]); + let tooltipResult = tooltip([{ profile_data: 0.13500013531, sample: "sampleID" }]); assert.isTrue(tooltipResult.html().indexOf("0.14") > -1, "correct result with no integer part"); - tooltipResult = tooltip([{ profile_data:6.100032, sample:"sampleID" }]); + tooltipResult = tooltip([{ profile_data: 6.100032, sample: "sampleID" }]); assert.isTrue(tooltipResult.html().indexOf("6.10") > -1, "correct result with integer part"); - tooltipResult = tooltip([{ profile_data: 0, sample:"sampleID" }]); + tooltipResult = tooltip([{ profile_data: 0, sample: "sampleID" }]); assert.isTrue(tooltipResult.html().indexOf("0.00") > -1, "correct result for zero") - tooltipResult = tooltip([{ profile_data:-0.13500013531, sample:"sampleID" }]); + tooltipResult = tooltip([{ profile_data: -0.13500013531, sample: "sampleID" }]); assert.isTrue(tooltipResult.html().indexOf("-0.14") > -1, "correct result with no integer part, negative"); - tooltipResult = tooltip([{ profile_data:-6.100032, sample:"sampleID" }]); + tooltipResult = tooltip([{ profile_data: -6.100032, sample: "sampleID" }]); assert.isTrue(tooltipResult.html().indexOf("-6.10") > -1, "correct result with integer part, negative"); // more than one data - tooltipResult = tooltip([{ profile_data:0.13500013531, sample:"sampleID" }, { profile_data:0.23500013531, sample:"sampleID" }, { profile_data:0.33500013531, sample:"sampleID" }]); + tooltipResult = tooltip([{ profile_data: 0.13500013531, sample: "sampleID" }, { profile_data: 0.23500013531, sample: "sampleID" }, { profile_data: 0.33500013531, sample: "sampleID" }]); assert.isTrue(tooltipResult.html().indexOf("0.24 (average of 3 values)") > -1, "multiple - correct result with no integer part"); - tooltipResult = tooltip([{ profile_data:6.100032, sample:"sampleID" }, { profile_data:8.100032, sample:"sampleID" }]); + tooltipResult = tooltip([{ profile_data: 6.100032, sample: "sampleID" }, { profile_data: 8.100032, sample: "sampleID" }]); assert.isTrue(tooltipResult.html().indexOf("7.10 (average of 2 values)") > -1, "multiple - correct result with integer part"); - tooltipResult = tooltip([{ profile_data: 0, sample:"sampleID" }, { profile_data: 0, sample:"sampleID" }, { profile_data: 0, sample:"sampleID" }, { profile_data: 0, sample:"sampleID" }]); + tooltipResult = tooltip([{ profile_data: 0, sample: "sampleID" }, { profile_data: 0, sample: "sampleID" }, { profile_data: 0, sample: "sampleID" }, { profile_data: 0, sample: "sampleID" }]); assert.isTrue(tooltipResult.html().indexOf("0.00 (average of 4 values)") > -1, "multiple - correct result for zero") - tooltipResult = tooltip([{ profile_data:-0.03500013531, sample:"sampleID" }, { profile_data:-0.03500013531, sample:"sampleID" }, { profile_data:-0.23500013531, sample:"sampleID" }, { profile_data:-0.23500013531, sample:"sampleID" }]); + tooltipResult = tooltip([{ profile_data: -0.03500013531, sample: "sampleID" }, { profile_data: -0.03500013531, sample: "sampleID" }, { profile_data: -0.23500013531, sample: "sampleID" }, { profile_data: -0.23500013531, sample: "sampleID" }]); assert.isTrue(tooltipResult.html().indexOf("-0.14 (average of 4 values)") > -1, "correct result with no integer part, negative"); - tooltipResult = tooltip([{ profile_data:-6.100032, sample:"sampleID" }, { profile_data:-2.100032, sample:"sampleID" }]); + tooltipResult = tooltip([{ profile_data: -6.100032, sample: "sampleID" }, { profile_data: -2.100032, sample: "sampleID" }]); assert.isTrue(tooltipResult.html().indexOf("-4.10 (average of 2 values)") > -1, "correct result with integer part, negative"); + + }); + + it('Should not handle categories for molecular genetic alterations', () => { + const tooltipResult = tooltip([{ profile_data: 8, sample: "sampleID", category: ">8.00" }]); + assert.isTrue(tooltipResult.html().indexOf("8.00") > -1, "molecular track - category is ignored when available"); }); + + const fTreamentTooltip = makeHeatmapTrackTooltip("GENERIC_ASSAY", false); + + it('Should handle categories for treatment genetic alterations', () => { + const tooltipResult = fTreamentTooltip([{ profile_data: 8, sample: "sampleID", category: ">8.00" }]); + assert.isTrue(tooltipResult.html().indexOf(">8.00") > -1, "treatment - category is displayed when available"); + }); + + it('Should handle categories for multiple treatment genetic alterations', () => { + const tooltipResult = fTreamentTooltip([{ profile_data: 8, sample: "sampleID", category: ">8.00" }, { profile_data: 7, sample: "sampleID", category: ">7.00" }]); + assert.isTrue(tooltipResult.html().indexOf(">8.00, >7.00 (2 data points)") > -1, "treatment - multiple categories are displayed when under mouse"); + }); + + it('Should handle single values and single category for multiple treatment genetic alterations', () => { + const tooltipResult = fTreamentTooltip([{ profile_data: 8, sample: "sampleID", category: "" }, { profile_data: 7, sample: "sampleID", category: ">7.00" }]); + assert.isTrue(tooltipResult.html().indexOf("8.00 and >7.00") > -1, "treatment - multiple categories are displayed when under mouse"); + }); + + it('Should handle multiple values and multiple categories for multiple treatment genetic alterations', () => { + const tooltipResult = fTreamentTooltip([{ profile_data: 6, sample: "sampleID", category: "" }, { profile_data: 8, sample: "sampleID", category: "" }, { profile_data: 7, sample: "sampleID", category: ">7.00" }, { profile_data: 7, sample: "sampleID", category: ">7.00" }, { profile_data: 9, sample: "sampleID", category: ">9.00" }]); + assert.isTrue(tooltipResult.html().indexOf("7.00 (average of 2 values) and >7.00, >9.00 (3 data points)") > -1, "treatment - multiple values and categories (unique) are displayed when under mouse"); + }); + }); - describe("makeGeneticTrackTooltip_getCoverageInformation", ()=>{ - it("gives correct results on undefined input", ()=>{ + describe("makeGeneticTrackTooltip_getCoverageInformation", () => { + it("gives correct results on undefined input", () => { assert.deepEqual( makeGeneticTrackTooltip_getCoverageInformation(undefined, undefined), { @@ -1058,7 +1089,7 @@ describe("Oncoprint TooltipUtils", ()=>{ } ); }); - it("gives correct results on empty input", ()=>{ + it("gives correct results on empty input", () => { assert.deepEqual( makeGeneticTrackTooltip_getCoverageInformation([], []), { @@ -1071,7 +1102,7 @@ describe("Oncoprint TooltipUtils", ()=>{ } ); }); - it("gives correct results with empty profiled_in but no not_profiled_in", ()=>{ + it("gives correct results with empty profiled_in but no not_profiled_in", () => { assert.deepEqual( makeGeneticTrackTooltip_getCoverageInformation([], undefined), { @@ -1084,7 +1115,7 @@ describe("Oncoprint TooltipUtils", ()=>{ } ); }); - it("gives correct results with empty not_profiled_in but no profiled_in", ()=>{ + it("gives correct results with empty not_profiled_in but no profiled_in", () => { assert.deepEqual( makeGeneticTrackTooltip_getCoverageInformation(undefined, []), { @@ -1097,9 +1128,9 @@ describe("Oncoprint TooltipUtils", ()=>{ } ); }); - it("gives correct results with nonempty profiled_in but no not_profiled_in", ()=>{ + it("gives correct results with nonempty profiled_in but no not_profiled_in", () => { assert.deepEqual( - makeGeneticTrackTooltip_getCoverageInformation([{genePanelId:"panel", molecularProfileId:"profile"}], undefined), + makeGeneticTrackTooltip_getCoverageInformation([{ genePanelId: "panel", molecularProfileId: "profile" }], undefined), { dispProfiledGenePanelIds: ["panel"], dispNotProfiledGenePanelIds: [], @@ -1110,9 +1141,9 @@ describe("Oncoprint TooltipUtils", ()=>{ } ); }); - it("gives correct results with nonempty not_profiled_in but no profiled_in", ()=>{ + it("gives correct results with nonempty not_profiled_in but no profiled_in", () => { assert.deepEqual( - makeGeneticTrackTooltip_getCoverageInformation(undefined, [{molecularProfileId:"profile"}]), + makeGeneticTrackTooltip_getCoverageInformation(undefined, [{ molecularProfileId: "profile" }]), { dispProfiledGenePanelIds: [], dispNotProfiledGenePanelIds: [], @@ -1123,9 +1154,9 @@ describe("Oncoprint TooltipUtils", ()=>{ } ); }); - it("gives correct results with nonempty profiled_in and empty not_profiled_in", ()=>{ + it("gives correct results with nonempty profiled_in and empty not_profiled_in", () => { assert.deepEqual( - makeGeneticTrackTooltip_getCoverageInformation([{molecularProfileId:"profile"}], []), + makeGeneticTrackTooltip_getCoverageInformation([{ molecularProfileId: "profile" }], []), { dispProfiledGenePanelIds: [], dispNotProfiledGenePanelIds: [], @@ -1136,9 +1167,9 @@ describe("Oncoprint TooltipUtils", ()=>{ } ); }); - it("gives correct results with nonempty not_profiled_in and empty profiled_in", ()=>{ + it("gives correct results with nonempty not_profiled_in and empty profiled_in", () => { assert.deepEqual( - makeGeneticTrackTooltip_getCoverageInformation([], [{molecularProfileId:"profile"}]), + makeGeneticTrackTooltip_getCoverageInformation([], [{ molecularProfileId: "profile" }]), { dispProfiledGenePanelIds: [], dispNotProfiledGenePanelIds: [], @@ -1149,9 +1180,9 @@ describe("Oncoprint TooltipUtils", ()=>{ } ); }); - it("gives correct results with nonoverlapping profiled_in and not_profiled_in", ()=>{ + it("gives correct results with nonoverlapping profiled_in and not_profiled_in", () => { assert.deepEqual( - makeGeneticTrackTooltip_getCoverageInformation([{genePanelId:"panel", molecularProfileId:"profile1"}], [{molecularProfileId:"profile"}]), + makeGeneticTrackTooltip_getCoverageInformation([{ genePanelId: "panel", molecularProfileId: "profile1" }], [{ molecularProfileId: "profile" }]), { dispProfiledGenePanelIds: ["panel"], dispNotProfiledGenePanelIds: [], @@ -1162,9 +1193,9 @@ describe("Oncoprint TooltipUtils", ()=>{ } ); }); - it("gives correct results with overlapping profiled_in and not_profiled_in", ()=>{ + it("gives correct results with overlapping profiled_in and not_profiled_in", () => { assert.deepEqual( - makeGeneticTrackTooltip_getCoverageInformation([{genePanelId:"panel", molecularProfileId:"profile"}], [{genePanelId:"panel2", molecularProfileId:"profile"}]), + makeGeneticTrackTooltip_getCoverageInformation([{ genePanelId: "panel", molecularProfileId: "profile" }], [{ genePanelId: "panel2", molecularProfileId: "profile" }]), { dispProfiledGenePanelIds: ["panel"], dispNotProfiledGenePanelIds: ["panel2"], diff --git a/src/shared/components/oncoprint/TooltipUtils.ts b/src/shared/components/oncoprint/TooltipUtils.ts index 457783a11e4..ba57bcae840 100644 --- a/src/shared/components/oncoprint/TooltipUtils.ts +++ b/src/shared/components/oncoprint/TooltipUtils.ts @@ -8,7 +8,8 @@ import client from "shared/api/cbioportalClientInstance"; import {ClinicalTrackSpec, GeneticTrackDatum} from "./Oncoprint"; import { AnnotatedExtendedAlteration, AnnotatedMutation, AnnotatedNumericGeneMolecularData, - ExtendedAlteration + ExtendedAlteration, + AlterationTypeConstants } from "../../../pages/resultsView/ResultsViewPageStore"; import _ from "lodash"; import {alterationTypeToProfiledForText} from "./ResultsViewOncoprintUtils"; @@ -23,6 +24,8 @@ const customDriverTiersImg = require("../../../rootImages/driver_tiers.png"); export const TOOLTIP_DIV_CLASS = "oncoprint__tooltip"; +const tooltipTextElementNaN = 'N/A'; + function sampleViewAnchorTag(study_id:string, sample_id:string) { return `${sample_id}`; } @@ -123,34 +126,72 @@ export function makeClinicalTrackTooltip(track:ClinicalTrackSpec, link_id?:boole } export function makeHeatmapTrackTooltip(genetic_alteration_type:MolecularProfile["molecularAlterationType"], link_id?:boolean) { return function (dataUnderMouse:any[]) { + let data_header = ''; - let profile_data = 'N/A'; + let valueTextElement = tooltipTextElementNaN; + let categoryTextElement = ''; + switch(genetic_alteration_type) { - case "MRNA_EXPRESSION": + case AlterationTypeConstants.MRNA_EXPRESSION: data_header = 'MRNA: '; break; - case "PROTEIN_LEVEL": + case AlterationTypeConstants.PROTEIN_LEVEL: data_header = 'PROT: '; break; - case "METHYLATION": + case AlterationTypeConstants.METHYLATION: data_header = 'METHYLATION: '; break; + case AlterationTypeConstants.GENERIC_ASSAY: + data_header = 'TREATMENT: '; + break; } + let profileDataSum = 0; + const profileCategories:string[] = []; let profileDataCount = 0; + let categoryCount = 0; for (const d of dataUnderMouse) { if ((d.profile_data !== null) && (typeof d.profile_data !== "undefined")) { - profileDataSum += d.profile_data; - profileDataCount += 1; + if (genetic_alteration_type === AlterationTypeConstants.GENERIC_ASSAY && d.category) { + profileCategories.push(d.category); + categoryCount += 1; + } else { + profileDataSum += d.profile_data; + profileDataCount += 1; + } } } + if (profileDataCount > 0) { - profile_data = (profileDataSum/profileDataCount).toFixed(2); - if (profileDataCount > 1) { - profile_data = `${profile_data} (average of ${profileDataCount} values)`; + const profileDisplayValue = (profileDataSum/profileDataCount).toFixed(2); + if (profileDataCount === 1) { + valueTextElement = profileDisplayValue; + } else { + valueTextElement = `${profileDisplayValue} (average of ${profileDataCount} values)`; } } - let ret = data_header + '' + profile_data + '
'; + + if (categoryCount > 0) { + if (profileDataCount === 0 && categoryCount === 1) { + categoryTextElement = profileCategories[0]; + } else if (profileDataCount > 0 && categoryCount === 1) { + categoryTextElement = `${profileCategories[0]}`; + } else { + categoryTextElement = `${_.uniq(profileCategories).join(", ")} (${categoryCount} data points)`; + } + } + + let ret = data_header; + if (valueTextElement !== tooltipTextElementNaN || categoryCount === 0) { + ret += '' + valueTextElement + ''; + } + if (valueTextElement !== tooltipTextElementNaN && categoryCount > 0) { + ret += ' and '; + } + if (categoryCount > 0) { + ret += '' + categoryTextElement + ''; + } + ret += '
'; return $('
').addClass(TOOLTIP_DIV_CLASS).append(getCaseViewElt(dataUnderMouse, !!link_id)).append("
").append(ret); }; }; diff --git a/src/shared/components/oncoprint/controls/OncoprintControls.spec.ts b/src/shared/components/oncoprint/controls/OncoprintControls.spec.ts new file mode 100644 index 00000000000..f62e655bcbe --- /dev/null +++ b/src/shared/components/oncoprint/controls/OncoprintControls.spec.ts @@ -0,0 +1,107 @@ +import {assert} from "chai"; +import OncoprintControls from 'shared/components/oncoprint/controls/OncoprintControls'; +import { Treatment } from "shared/api/generated/CBioPortalAPIInternal"; + +describe('treatmentSelectOptions', () => { + + it('Includes entity_stable_id and description when present and unique', () => { + + const treatments = [ + { + treatmentId: 'id_1', + name: 'name_1', + description: 'desc_1' + }, + { + treatmentId: 'id_2', + name: 'name_2', + description: 'desc_2' + } + ] as Treatment[]; + + const props = { state: { treatmentsPromise: { result: treatments} } }; + const controls = new OncoprintControls(props as any); + + const expect = [ + {id: 'id_1', value: 'name_1 (id_1): desc_1', label: 'name_1 (id_1): desc_1'}, + {id: 'id_2', value: 'name_2 (id_2): desc_2', label: 'name_2 (id_2): desc_2'} + ]; + assert.deepEqual(controls.treatmentSelectOptions, expect); + }); + + it('Hides description when same as entity_stable_id', () => { + + const treatments = [ + { + treatmentId: 'id_1', + name: 'name_1', + description: 'id_1' + }, + { + treatmentId: 'id_2', + name: 'name_2', + description: 'id_2' + } + ] as Treatment[]; + + const props = { state: { treatmentsPromise: { result: treatments} } }; + const controls = new OncoprintControls(props as any); + + const expect = [ + {id: 'id_1', value: 'name_1 (id_1)', label: 'name_1 (id_1)'}, + {id: 'id_2', value: 'name_2 (id_2)', label: 'name_2 (id_2)'} + ]; + assert.deepEqual(controls.treatmentSelectOptions, expect); + }); + + it('Hides entity_stable_id when same as name', () => { + + const treatments = [ + { + treatmentId: 'id_1', + name: 'name_1', + description: 'id_1' + }, + { + treatmentId: 'id_2', + name: 'name_2', + description: 'id_2' + } + ] as Treatment[]; + + const props = { state: { treatmentsPromise: { result: treatments} } }; + const controls = new OncoprintControls(props as any); + + const expect = [ + {id: 'id_1', value: 'name_1 (id_1)', label: 'name_1 (id_1)'}, + {id: 'id_2', value: 'name_2 (id_2)', label: 'name_2 (id_2)'} + ]; + assert.deepEqual(controls.treatmentSelectOptions, expect); + }); + + it('Hides name and description when same as entity_stable_id', () => { + + const treatments = [ + { + treatmentId: 'id_1', + name: 'id_1', + description: 'id_1' + }, + { + treatmentId: 'id_2', + name: 'id_2', + description: 'id_2' + } + ] as Treatment[]; + + const props = { state: { treatmentsPromise: { result: treatments} } }; + const controls = new OncoprintControls(props as any); + + const expect = [ + {id: 'id_1', value: 'id_1', label: 'id_1'}, + {id: 'id_2', value: 'id_2', label: 'id_2'} + ]; + assert.deepEqual(controls.treatmentSelectOptions, expect); + }); + +}); \ No newline at end of file diff --git a/src/shared/components/oncoprint/controls/OncoprintControls.tsx b/src/shared/components/oncoprint/controls/OncoprintControls.tsx index d5f8ec8ceb2..764ae89c1d1 100644 --- a/src/shared/components/oncoprint/controls/OncoprintControls.tsx +++ b/src/shared/components/oncoprint/controls/OncoprintControls.tsx @@ -4,7 +4,7 @@ import {Button, ButtonGroup} from "react-bootstrap"; import CustomDropdown from "./CustomDropdown"; import ReactSelect from "react-select"; import {MobxPromise} from "mobxpromise"; -import {action, computed, IObservableObject, observable, ObservableMap, reaction} from "mobx"; +import {action, computed, IObservableObject, observable, ObservableMap, reaction, toJS} from "mobx"; import _ from "lodash"; import {SortMode} from "../ResultsViewOncoprint"; import {Gene, MolecularProfile} from "shared/api/generated/CBioPortalAPI"; @@ -17,7 +17,7 @@ import "./styles.scss"; import ErrorIcon from "../../ErrorIcon"; import classNames from "classnames"; import {SpecialAttribute} from "../../../cache/ClinicalDataCache"; -import {ResultsViewPageStore} from "../../../../pages/resultsView/ResultsViewPageStore"; +import {ResultsViewPageStore, AlterationTypeConstants} from "../../../../pages/resultsView/ResultsViewPageStore"; import {ExtendedClinicalAttribute} from "../../../../pages/resultsView/ResultsViewPageStoreUtils"; import {getNCBIlink} from "public-lib/lib/urls"; import {GeneBoxType} from "../../GeneSelectionBox/GeneSelectionBox"; @@ -25,6 +25,10 @@ import GeneSelectionBox from "../../GeneSelectionBox/GeneSelectionBox"; import autobind from "autobind-decorator"; import {SingleGeneQuery} from "../../../lib/oql/oql-parser"; import AddClinicalTracks from "../../../../pages/resultsView/oncoprint/AddClinicalTracks"; +import {Treatment} from "shared/api/generated/CBioPortalAPIInternal"; +import TextIconArea, { ITextIconAreaItemProps } from "shared/components/textIconArea/TextIconArea"; +import { extractTreatmentSelections } from "../OncoprintUtils"; +import {CheckedSelect} from "react-select-checked"; export interface IOncoprintControlsHandlers { onSelectColumnType?:(type:"sample"|"patient")=>void, @@ -58,10 +62,12 @@ export interface IOncoprintControlsHandlers { onChangeSelectedClinicalTracks?:(attributeIds:(string|SpecialAttribute)[])=>void; onClickAddGenesToHeatmap?:()=>void; + onClickAddTreatmentsToHeatmap?:(treatments:string[])=>void; onClickRemoveHeatmap?:()=>void; onClickClusterHeatmap?:()=>void; onSelectHeatmapProfile?:(molecularProfileId:string)=>void; onChangeHeatmapGeneInputValue?:(value:string)=>void; + onChangeHeatmapTreatmentInputValue?:(value:string)=>void; onSetHorzZoom:(z:number)=>void; onClickZoomIn:()=>void; @@ -99,9 +105,12 @@ export interface IOncoprintControlsState { clinicalAttributeSampleCountPromise?:MobxPromise<{[clinicalAttributeId:string]:number}>, selectedClinicalAttributeIds?:string[], heatmapProfilesPromise?:MobxPromise, + treatmentsPromise?:MobxPromise selectedHeatmapProfile?:string; + selectedHeatmapProfileAlterationType?:string; heatmapIsDynamicallyQueried?:boolean; heatmapGeneInputValue?: string; + heatmapTreatmentInputValue?:string; clusterHeatmapButtonActive?:boolean; hideClusterHeatmapButton?:boolean; hideHeatmapMenu?:boolean; @@ -124,6 +133,12 @@ export interface IOncoprintControlsProps { oncoprinterMode?:boolean; } +export interface ISelectOption { + id:string, + value:string, + label:string +} + const EVENT_KEY = { columnTypeSample: "0", columnTypePatient: "1", @@ -159,12 +174,16 @@ const EVENT_KEY = { downloadOrder:"28", downloadTabular:"29", horzZoomSlider:"30", + addTreatmentsToHeatmap: "32" }; @observer export default class OncoprintControls extends React.Component { + @observable horzZoomSliderState:number; - @observable heatmapGenesReady:boolean = false; + @observable heatmapGenesReady = false; + @observable private _selectedTreatmentOptionsObsArray:ISelectOption[] = []; + private textareaTreatmentText = ""; constructor(props:IOncoprintControlsProps) { super(props); @@ -337,9 +356,14 @@ export default class OncoprintControls extends React.Component d.id !== treatmentId; + this._selectedTreatmentOptionsObsArray = _.filter(this._selectedTreatmentOptionsObsArray, removeTreatmentByIdFilter); + } + @computed get heatmapProfileOptions() { if (this.props.state.heatmapProfilesPromise && this.props.state.heatmapProfilesPromise.result) { return _.map(this.props.state.heatmapProfilesPromise.result, profile=>({ label: profile.name, - value: profile.molecularProfileId + value: profile.molecularProfileId, + type: profile.molecularAlterationType })); } else { return []; } } + @computed get treatmentSelectOptions():ISelectOption[] { + // Note: name and desc are optional fields for treatment entities + // When not provided in the data file, these fields are assigned the + // value of the entity_stable_id. The code below hides fields when + // indentical to the entity_stable_id. + if (this.props.state.treatmentsPromise && this.props.state.treatmentsPromise.result) { + return _.map(this.props.state.treatmentsPromise.result, (d:Treatment) => { + const uniqueName = d.name !== d.treatmentId; + const uniqueDesc = d.description !== d.treatmentId && d.description !== d.name; + let label = ""; + if (!uniqueName && !uniqueDesc) { + label = d.treatmentId; + } else if (!uniqueName) { + label = `${d.treatmentId}: ${d.description}`; + } else if (!uniqueDesc) { + label = `${d.name} (${d.treatmentId})`; + } else { + label = `${d.name} (${d.treatmentId}): ${d.description}`; + } + + // For searching, react-select-checked performs a search in the value + // field and displays the label field. To allow searching in all words + // that appear in the label field, the value field is made identical to + // the label field. The id field is added to track the unique identifier + // of the treatment. + return { + id: d.treatmentId, + value: label, + label: label + }; + }); + } else { + return []; + } + } + + @computed get treatmentOptionsByValueMap():{[value:string]: ISelectOption}{ + return _.keyBy(this.treatmentSelectOptions, 'id'); + } + + @autobind + private onSelectTreatments(selectedElements:any[]) { + this._selectedTreatmentOptionsObsArray = selectedElements; + } + + @computed get selectedTreatments() { + return toJS(this._selectedTreatmentOptionsObsArray); + } + + @computed get textareaTreatmentEntries():ITextIconAreaItemProps[] { + return _.map(this._selectedTreatmentOptionsObsArray, (d:ISelectOption) => ({value: d.id, label: d.id})); + } + private getClinicalTracksMenu() { // TODO: put onFocus handler on CheckedSelect when possible // TODO: pass unmodified string array as value prop when possible @@ -424,6 +513,11 @@ export default class OncoprintControls extends React.Component; } @@ -442,8 +536,8 @@ export default class OncoprintControls extends React.Component - {this.props.state.heatmapIsDynamicallyQueried && [ - Add Genes to Heatmap, - + >Add Genes to Heatmap + ] + } + {showTreatmentsTextArea && this.props.state.treatmentsPromise!.isComplete && + [, +
+ +
, - ]} + >Add Treatments to Heatmap + ] + } + + {!this.props.state.hideClusterHeatmapButton && () + className={classNames("btn", "btn-sm", "btn-default", { active: this.props.state.clusterHeatmapButtonActive })} + name={EVENT_KEY.sortByHeatmapClustering} + onClick={this.onButtonClick} + >Cluster Heatmap) }
); diff --git a/src/shared/components/oncoprint/controls/styles.scss b/src/shared/components/oncoprint/controls/styles.scss index 7aa16e5105c..7c5663c7d19 100644 --- a/src/shared/components/oncoprint/controls/styles.scss +++ b/src/shared/components/oncoprint/controls/styles.scss @@ -2,6 +2,8 @@ .oncoprint__controls { + display:flex; + .rangeslider { margin:0; .rangeslider__labels { @@ -15,18 +17,17 @@ } } } - + .checkbox, .radio { &:first-child { margin-top:0px; } } - .dropdown-menu { width:270px; padding:12px !important; > *, button { - margin-bottom:10px !important; + margin-bottom:10px; width:100%; } > :last-child { @@ -34,9 +35,9 @@ } &.heatmap { width:450px; - } + } } - + .oncoprint__controls__heatmap_menu { textarea { width:100%; @@ -45,45 +46,131 @@ resize: vertical; } > *, button { - margin-bottom:10px !important; + margin-bottom:10px; width:100%; } > :last-child { margin-bottom:0 !important; } - } + .treatment-selector { + + .Select { + + margin-right:0px; + + .Select-control { + height:32px; + } + + .Select-input { + height:32px; + } + + .Select-menu-outer { + width: 100%; + max-height: 400px; + + .Select-menu { + max-height: 398px; // needs to be 2 less than Select-menu-outer height to not hide border. Irrelevant + // as long as & > div > div:nth-child(2) max-height is less + overflow:hidden; + + button { + width: auto; + margin: 0; + padding: 0; + vertical-align: middle; + } + + & > div > div:nth-child(1){ + padding: 0; + } + + & > div > div:nth-child(2){ + max-height:350px !important; + overflow-x:hidden !important; + overflow-y:scroll !important; + } + + } + + } + } + + } + + .text-icon-area { + width:100%; + min-height:50px; + overflow: hidden; + margin-bottom: 5px; + + $iconMargin: 2px; + $iconPadding: 1px; + + .icon-area { + min-height:0px; + overflow: hidden; + padding: 1px; + padding-bottom: 2px; + + .icon { + padding: $iconPadding; + padding-left: $iconPadding+6; + padding-right: $iconPadding+6; + margin: $iconMargin; + margin-bottom: $iconMargin+1; + background: $brand-primary; + color: white; + border-radius: 4px; + float: left; + } + + } + + .text-area { + width:100%; + min-height: 50px; + resize: vertical; + overflow: hidden; + border:1px solid $borderColor; + } + + } + } + .oncoprint__zoom-controls { display:flex !important; align-items:center; - + > * { margin-right:10px; } - + > :last-child { margin-right:0; } - + } - + .oncoprint__controls__minimap_button { padding: 5px 8px 6px 10px !important; } - + .Select { - + margin-right:10px; - + .Select-control { height:32px; } - + .Select-input { height:32px; } - + } .clinical-track-selector { diff --git a/src/shared/components/oncoprint/tabularDownload.ts b/src/shared/components/oncoprint/tabularDownload.ts index af2511ea633..4dad9ebca95 100644 --- a/src/shared/components/oncoprint/tabularDownload.ts +++ b/src/shared/components/oncoprint/tabularDownload.ts @@ -1,10 +1,12 @@ -import Oncoprint, {ClinicalTrackSpec, GeneticTrackSpec, IGeneHeatmapTrackSpec} from "./Oncoprint"; +import Oncoprint, {ClinicalTrackSpec, GeneticTrackSpec, IHeatmapTrackSpec, IGenesetHeatmapTrackSpec, IBaseHeatmapTrackSpec} from "./Oncoprint"; import fileDownload from "react-file-download"; export default function tabularDownload( geneticTracks:GeneticTrackSpec[], clinicalTracks:ClinicalTrackSpec[], - heatmapTracks:IGeneHeatmapTrackSpec[], + heatmapTracks: IHeatmapTrackSpec[], + treatmentHeatmapTracks: IHeatmapTrackSpec[], + genesetTracks: IGenesetHeatmapTrackSpec[], uidOrder:string[], getCaseId:(uid:string)=>string, columnMode:"sample"|"patient", @@ -131,7 +133,8 @@ export default function tabularDownload( } //Add heatmap data - for (const heatmapTrack of heatmapTracks) { + const exportedHeatmapTracks = (heatmapTracks as IBaseHeatmapTrackSpec[]).concat(treatmentHeatmapTracks as IBaseHeatmapTrackSpec[]).concat(genesetTracks as IBaseHeatmapTrackSpec[]); + for (const heatmapTrack of exportedHeatmapTracks) { const currentHeatmapGene = heatmapTrack.label; const currentHeatmapType = "HEATMAP "+heatmapTrack.molecularAlterationType+' '+ heatmapTrack.datatype; const currentHeatmapTrackData = heatmapTrack.data; diff --git a/src/shared/components/plots/BoxScatterPlot.tsx b/src/shared/components/plots/BoxScatterPlot.tsx index 2ba0504e4d3..541779d163a 100644 --- a/src/shared/components/plots/BoxScatterPlot.tsx +++ b/src/shared/components/plots/BoxScatterPlot.tsx @@ -665,4 +665,4 @@ export default class BoxScatterPlot extends ); } -} \ No newline at end of file +} diff --git a/src/shared/components/plots/PlotUtils.ts b/src/shared/components/plots/PlotUtils.ts index 117062ae5b9..a06fd9fa754 100644 --- a/src/shared/components/plots/PlotUtils.ts +++ b/src/shared/components/plots/PlotUtils.ts @@ -247,4 +247,4 @@ export function computeCorrelationPValue(correlation:number, numSamples:number) } else { return null; } -} \ No newline at end of file +} diff --git a/src/shared/components/plots/ScatterPlot.tsx b/src/shared/components/plots/ScatterPlot.tsx index 3a47d7cfd52..0c4b7efc204 100644 --- a/src/shared/components/plots/ScatterPlot.tsx +++ b/src/shared/components/plots/ScatterPlot.tsx @@ -478,4 +478,4 @@ export default class ScatterPlot extends React.C ); } -} \ No newline at end of file +} diff --git a/src/shared/components/query/QueryStore.ts b/src/shared/components/query/QueryStore.ts index 476ee6285b9..4668fec3e52 100644 --- a/src/shared/components/query/QueryStore.ts +++ b/src/shared/components/query/QueryStore.ts @@ -60,6 +60,7 @@ export type CancerStudyQueryUrlParams = { genetic_profile_ids_PROFILE_METHYLATION: string, genetic_profile_ids_PROFILE_PROTEIN_EXPRESSION: string, genetic_profile_ids_PROFILE_GENESET_SCORE: string, + genetic_profile_ids_PROFILE_GENERIC_ASSAY: string, Z_SCORE_THRESHOLD: string, RPPA_SCORE_THRESHOLD: string, data_priority: '0' | '1' | '2', @@ -67,6 +68,7 @@ export type CancerStudyQueryUrlParams = { case_ids: string, gene_list: string, geneset_list?: string, + treatment_list?: string, tab_index: 'tab_download' | 'tab_visualize', transpose_matrix?: 'on', Action: 'Submit', @@ -96,7 +98,8 @@ export type CancerStudyQueryParams = Pick; + 'genesetQuery' | + 'treatmentQuery'>; export const QueryParamsKeys: (keyof CancerStudyQueryParams)[] = [ 'searchText', 'selectableSelectedStudyIds', @@ -109,6 +112,7 @@ export const QueryParamsKeys: (keyof CancerStudyQueryParams)[] = [ 'caseIdsMode', 'geneQuery', 'genesetQuery', + 'treatmentQuery' ]; type GenesetId = string; @@ -380,6 +384,18 @@ export class QueryStore { // clear error when gene query is modified this.genesetQueryErrorDisplayStatus = 'unfocused'; this._genesetQuery = value; + } + + @observable _treatmentQuery = ''; + get treatmentQuery() + { + return this._treatmentQuery; + } + set treatmentQuery(value:string) + { + // clear error when gene query is modified + this.treatmentQueryErrorDisplayStatus = 'unfocused'; + this._treatmentQuery = value; } //////////////////////////////////////////////////////////////////////////////// @@ -388,6 +404,7 @@ export class QueryStore { @observable geneQueryErrorDisplayStatus: 'unfocused' | 'shouldFocus' | 'focused' = 'unfocused'; @observable genesetQueryErrorDisplayStatus: 'unfocused' | 'shouldFocus' | 'focused' = 'unfocused'; + @observable treatmentQueryErrorDisplayStatus: 'unfocused' | 'shouldFocus' | 'focused' = 'unfocused'; @observable showMutSigPopup = false; @observable showGisticPopup = false; @observable showGenesetsHierarchyPopup = false; @@ -1534,6 +1551,7 @@ export class QueryStore { params.genetic_profile_ids_PROFILE_METHYLATION, params.genetic_profile_ids_PROFILE_PROTEIN_EXPRESSION, params.genetic_profile_ids_PROFILE_GENESET_SCORE, + params.genetic_profile_ids_PROFILE_GENERIC_ASSAY ]; let queriedStudies = params.cancer_study_list ? params.cancer_study_list.split(",") : (params.cancer_study_id ? [params.cancer_study_id] : []); @@ -1549,6 +1567,7 @@ export class QueryStore { this.caseIdsMode = 'sample'; // url always contains sample IDs this.geneQuery = normalizeQuery(decodeURIComponent(params.gene_list || '')); this.genesetQuery = normalizeQuery(decodeURIComponent(params[QueryParameter.GENESET_LIST] || '')); + this.treatmentQuery = decodeURIComponent(params[QueryParameter.TREATMENT_LIST] || ''); // pvannierop: removed the conversion to uppercase this.forDownloadTab = params.tab_index === 'tab_download'; this.initiallySelected.profileIds = true; this.initiallySelected.sampleListId = true; diff --git a/src/shared/components/query/QueryStoreUtils.ts b/src/shared/components/query/QueryStoreUtils.ts index 175a0355129..7f643c71a2e 100644 --- a/src/shared/components/query/QueryStoreUtils.ts +++ b/src/shared/components/query/QueryStoreUtils.ts @@ -6,12 +6,13 @@ import { VirtualStudy } from "shared/model/VirtualStudy"; export type NonMolecularProfileQueryParams = Pick; + 'case_set_id' | 'case_ids' | 'gene_list' | 'geneset_list' | 'treatment_list' | 'tab_index' | 'transpose_matrix' | 'Action'>; export type MolecularProfileQueryParams = Pick; + 'genetic_profile_ids_PROFILE_PROTEIN_EXPRESSION' | 'genetic_profile_ids_PROFILE_GENESET_SCORE' | + 'genetic_profile_ids_PROFILE_GENERIC_ASSAY' >; export function currentQueryParams(store:QueryStore) { @@ -52,6 +53,7 @@ export function nonMolecularProfileParams(store:QueryStore, whitespace_separated case_ids, gene_list: encodeURIComponent(normalizeQuery(store.geneQuery) || ' '), // empty string won't work geneset_list: normalizeQuery(store.genesetQuery) || ' ', //empty string won't work + treatment_list: normalizeQuery(store.treatmentQuery) || ' ', //empty string won't work tab_index: store.forDownloadTab ? 'tab_download' : 'tab_visualize' as any, transpose_matrix: store.transposeDataMatrix ? 'on' : undefined, Action: 'Submit', @@ -71,7 +73,8 @@ export function molecularProfileParams(store:QueryStore, molecularProfileIds?:Re genetic_profile_ids_PROFILE_MRNA_EXPRESSION: store.getSelectedProfileIdFromMolecularAlterationType("MRNA_EXPRESSION", molecularProfileIds), genetic_profile_ids_PROFILE_METHYLATION: store.getSelectedProfileIdFromMolecularAlterationType("METHYLATION", molecularProfileIds) || store.getSelectedProfileIdFromMolecularAlterationType("METHYLATION_BINARY", molecularProfileIds), genetic_profile_ids_PROFILE_PROTEIN_EXPRESSION: store.getSelectedProfileIdFromMolecularAlterationType("PROTEIN_LEVEL", molecularProfileIds), - genetic_profile_ids_PROFILE_GENESET_SCORE: store.getSelectedProfileIdFromMolecularAlterationType("GENESET_SCORE", molecularProfileIds) + genetic_profile_ids_PROFILE_GENESET_SCORE: store.getSelectedProfileIdFromMolecularAlterationType("GENESET_SCORE", molecularProfileIds), + genetic_profile_ids_PROFILE_GENERIC_ASSAY: store.getSelectedProfileIdFromMolecularAlterationType("GENERIC_ASSAY", molecularProfileIds) }; } diff --git a/src/shared/components/textIconArea/TextIconArea.tsx b/src/shared/components/textIconArea/TextIconArea.tsx new file mode 100644 index 00000000000..caa3f4ca1df --- /dev/null +++ b/src/shared/components/textIconArea/TextIconArea.tsx @@ -0,0 +1,112 @@ +import * as React from 'react'; +import { observer } from 'mobx-react'; +import classNames from 'classnames'; +import _ from 'lodash'; +import ReactDOM from 'react-dom'; +import { observable } from 'mobx'; +import autobind from "autobind-decorator"; + +export interface ITextIconAreaProps { + elements: ITextIconAreaItemProps[]; + text:string; + placeholder?: string; + classNames?: string[]; + onIconClicked?: (itemValue:string) => void; + onChangeTextArea?: (textAreaContents:string) => string; +} + +export interface ITextIconAreaItemProps { + label: string; + value: string; + classNames?: string[]; +} + +// This class proves an icon area at the top and a managed textarea at the bottom. +// The contents of the text area are passed to the parent after a set time delay. +// The parent can provide entries back to the component that are displayed as icons in the +// icon area. Icons in the icon area contain a button that when pressed informs the +// parent of the event. The parent can use this for instance for removing this item +// from the icon area. + +@observer +class TextIconArea extends React.Component { + + // Shomehow the textare is not able to listen to updates of this field + // from the parent with MobX. Instead, the parent callback 'onChangeTextArea' + // returns a string that is used to update the textarea. + @observable textAreaContent:string = ""; + timeout:NodeJS.Timer|undefined = undefined; + TIMEOUT_DELAY = 750; // milliseconds + + constructor(props:ITextIconAreaProps) { + super(props); + this.textUpdatedByUser = this.textUpdatedByUser.bind(this); + this.itemRemovedByUser = this.itemRemovedByUser.bind(this); + } + + private itemRemovedByUser = (event:any) => { + if (this.props.onIconClicked && event.target) { + this.props.onIconClicked(event.target.id); + } + } + + private textUpdatedByUser = (event:any) => { + this.textAreaContent = event.currentTarget.value; + if (this.props.onChangeTextArea) { + this.startTimedSubmit(); + } + } + + private startTimedSubmit() { + this.stopTimedSubmit(); + + this.timeout = setTimeout(function() { + this.textAreaContent = this.props.onChangeTextArea(this.textAreaContent); + this.stopTimedSubmit(); + }.bind(this), this.TIMEOUT_DELAY) + + } + + private stopTimedSubmit() { + if (this.timeout) { + clearTimeout(this.timeout); + } + } + + private onAreaClicked = (event:any) => { + const textArea:any = ReactDOM.findDOMNode(this.refs.textarea); + textArea.focus(); + } + + render() { + return ( +
+
+ {this.props.elements.map((element:ITextIconAreaItemProps) => { + return ( +
+ {element.label} +   +
+
+ ) + })} +
+ +
+ ) + } +} + +export default TextIconArea; \ No newline at end of file diff --git a/src/shared/lib/ExtendedRouterStore.ts b/src/shared/lib/ExtendedRouterStore.ts index b1d82dfb069..aaca7091d31 100644 --- a/src/shared/lib/ExtendedRouterStore.ts +++ b/src/shared/lib/ExtendedRouterStore.ts @@ -60,6 +60,7 @@ export enum QueryParameter { CANCER_STUDY_ID="cancer_study_id", DATA_PRIORITY="data_priority", GENESET_LIST="geneset_list", + TREATMENT_LIST="treatment_list", TAB_INDEX="tab_index", TRANSPOSE_MATRIX="transpose_matrix", ACTION="Action"