import * as React from 'react'; import _ from 'lodash'; import { observer, Observer } from 'mobx-react'; import './styles.scss'; import { CancerStudy, Gene, NumericGeneMolecularData, MolecularProfile, } from 'cbioportal-ts-api-client'; import { computed, observable, makeObservable } from 'mobx'; import { ExpressionStyle, expressionTooltip, getPossibleRNASeqVersions, } from './expressionHelpers'; import { Modal } from 'react-bootstrap'; import autobind from 'autobind-decorator'; import { ITooltipModel } from '../../../shared/model/ITooltipModel'; import ChartContainer from '../../../shared/components/ChartContainer/ChartContainer'; import { If, Then, Else } from 'react-if'; import jStat from 'jStat'; import classNames from 'classnames'; import { MSKTab, MSKTabs } from '../../../shared/components/MSKTabs/MSKTabs'; import { isPanCanStudy, isTCGAProvStudy } from '../ResultsViewPageStoreUtils'; import { CoverageInformation } from '../../../shared/lib/GenePanelUtils'; import { CNA_STROKE_WIDTH, IBoxScatterPlotPoint, INumberAxisData, IPlotSampleData, IStringAxisData, makeBoxScatterPlotData, makeScatterPlotPointAppearance, scatterPlotLegendData, scatterPlotZIndexSortBy, IAxisLogScaleParams, basicAppearance, } from '../plots/PlotsTabUtils'; import { ResultsViewPageStore } from '../ResultsViewPageStore'; import OqlStatusBanner from '../../../shared/components/banners/OqlStatusBanner'; import { remoteData, stringListToSet } from 'cbioportal-frontend-commons'; import MobxPromiseCache from '../../../shared/lib/MobxPromiseCache'; import { MobxPromise } from 'mobxpromise'; import LoadingIndicator from 'shared/components/loadingIndicator/LoadingIndicator'; import BoxScatterPlot, { IBoxScatterPlotData, } from '../../../shared/components/plots/BoxScatterPlot'; import { ColoringType, PlotType, SelectedColoringTypes, } from '../plots/PlotsTab'; import AlterationFilterWarning from '../../../shared/components/banners/AlterationFilterWarning'; import CaseFilterWarning from '../../../shared/components/banners/CaseFilterWarning'; import { getBoxWidth } from 'shared/lib/boxPlotUtils'; import { AnnotatedMutation } from 'shared/model/AnnotatedMutation'; export interface ExpressionWrapperProps { store: ResultsViewPageStore; expressionProfiles: MobxPromise; numericGeneMolecularDataCache: MobxPromiseCache< { entrezGeneId: number; molecularProfileId: string }, NumericGeneMolecularData[] >; studyMap: { [studyId: string]: CancerStudy }; genes: Gene[]; mutations: AnnotatedMutation[]; coverageInformation: CoverageInformation; } class ExpressionTabBoxPlot extends BoxScatterPlot {} const SVG_ID = 'expression-tab-plot-svg'; export type ScatterPoint = { x: number; y: NumericGeneMolecularData }; type SortOptions = 'alphabetic' | 'median'; type ExpressionTooltipModel = { studyName: string; studyId: string; mutationType: string | null; proteinChange: string | null; sampleId: string; expression: number; style: ExpressionStyle; }; @observer export default class ExpressionWrapper extends React.Component< ExpressionWrapperProps, {} > { constructor(props: ExpressionWrapperProps) { super(props); makeObservable(this); this.selectedGene = props.genes[0]; (window as any).box = this; } componentWillMount() { // initialize selected study state. study is on except if it does not have any data (doesn't appear in data collection) this.selectedStudyIds = _.mapValues(this.props.studyMap, study => true); } svgContainer: SVGElement; @observable private _rnaSeqVersion: 'rna_seq_mrna' | 'rna_seq_v2_mrna'; @observable.ref tooltipModel: ITooltipModel< ExpressionTooltipModel > | null = null; @observable.ref selectedGene: Gene; @observable studySelectorModalVisible = false; @observable showMutations: boolean = true; @observable showCna: boolean = false; @observable selectedStudyIds: { [studyId: string]: boolean } = {}; @observable height = 700; @observable logScale = true; @observable sortBy: SortOptions = 'alphabetic'; @observable isTooltipHovered = false; // for debugging @observable public showMutationType = []; tooltipCounter: number = 0; @computed get selectedStudies() { return _.filter( this.props.studyMap, study => this.selectedStudyIds[study.studyId] === true ); } @computed get widthThreshold() { return this.selectedStudies.length > 7; } @computed get boxWidth() { return getBoxWidth(this.selectedStudies.length); } readonly sampleStudyData = remoteData({ await: () => [this.props.store.samples], invoke: () => { // data representing the horizontal axis - which sample is in which study return Promise.resolve({ data: this.props.store.samples.result!.reduce( (_data, sample) => { // filter out data for unselected studies if (this.selectedStudyIds[sample.studyId]) { _data.push({ uniqueSampleKey: sample.uniqueSampleKey, value: this.props.studyMap[sample.studyId].name, }); } return _data; }, [] as IStringAxisData['data'] ), datatype: 'string', }); }, }); @computed get selectedRNASeqVersion() { return this._rnaSeqVersion ? this._rnaSeqVersion : this.possibleRNASeqVersions[0].value; } @computed get possibleRNASeqVersions() { if (this.props.expressionProfiles.isComplete) { return getPossibleRNASeqVersions( this.props.expressionProfiles.result! ); } return []; } readonly selectedExpressionProfiles = remoteData({ await: () => [this.props.expressionProfiles], invoke: () => { return Promise.resolve( _.filter( this.props.expressionProfiles.result, expressionProfile => { return RegExp( `${this.selectedRNASeqVersion}$|pan_can_atlas_2018_${this.selectedRNASeqVersion}_median$` ).test(expressionProfile.molecularProfileId); } ) ); }, }); readonly expressionData = remoteData({ await: () => this.props.numericGeneMolecularDataCache.await( [this.selectedExpressionProfiles], profiles => profiles.map((p: MolecularProfile) => ({ entrezGeneId: this.selectedGene.entrezGeneId, molecularProfileId: p.molecularProfileId, })) ), invoke: () => { // TODO: this cache is leading to some ugly code return Promise.resolve( _.flatten( this.props.numericGeneMolecularDataCache .getAll( this.selectedExpressionProfiles.result!.map(p => ({ entrezGeneId: this.selectedGene.entrezGeneId, molecularProfileId: p.molecularProfileId, })) ) .map(promise => promise.result!) ) as NumericGeneMolecularData[] ); }, }); readonly studiesWithExpressionData = remoteData<{ [studyId: string]: boolean; }>({ await: () => [this.expressionData], invoke: () => { const studyIds = _.chain( this.expressionData.result! ) .map(d => d.studyId) .uniq() .value(); return Promise.resolve(stringListToSet(studyIds)); }, }); readonly mutationDataExists = remoteData({ await: () => [this.props.store.studyToMutationMolecularProfile], invoke: () => { return Promise.resolve( !!_.values(this.props.store.studyToMutationMolecularProfile) .length ); }, }); readonly cnaDataExists = remoteData({ await: () => [this.props.store.studyToMolecularProfileDiscreteCna], invoke: () => { return Promise.resolve( !!_.values(this.props.store.studyToMolecularProfileDiscreteCna) .length ); }, }); readonly cnaData = remoteData({ await: () => { if (this.cnaDataShown && this.selectedGene !== undefined) { return [ this.props.store.annotatedCnaCache.get({ entrezGeneId: this.selectedGene.entrezGeneId, }), ]; } else { return []; } }, invoke: () => { if (this.cnaDataShown && this.selectedGene !== undefined) { return Promise.resolve( this.props.store.annotatedCnaCache.get({ entrezGeneId: this.selectedGene.entrezGeneId, }).result! ); } else { return Promise.resolve([]); } }, }); @computed get mutationDataShown() { return this.mutationDataExists.result && this.showMutations; } @computed get cnaDataShown() { return this.cnaDataExists.result && this.showCna; } readonly _unsortedBoxPlotData = remoteData({ await: () => [ this.sampleStudyData, this.expressionData, this.props.store.sampleKeyToSample, this.props.store.studyToMutationMolecularProfile, this.props.store.studyToMolecularProfileDiscreteCna, this.cnaData, ], invoke: () => { return Promise.resolve( makeBoxScatterPlotData( this.sampleStudyData.result!, { data: this.expressionData.result!, datatype: 'number', hugoGeneSymbol: this.selectedGene.hugoGeneSymbol, } as INumberAxisData, this.props.store.sampleKeyToSample.result!, this.props.coverageInformation.samples, this.mutationDataExists.result ? { molecularProfileIds: _.values( this.props.store .studyToMutationMolecularProfile.result! ).map(p => p.molecularProfileId), data: this.props.mutations.filter( m => m.entrezGeneId === this.selectedGene.entrezGeneId ), } : undefined, this.cnaDataShown ? { molecularProfileIds: _.values( this.props.store .studyToMolecularProfileDiscreteCna .result! ).map(p => p.molecularProfileId), data: this.cnaData.result!, } : undefined ) ); }, }); readonly boxPlotData = remoteData({ await: () => [this._unsortedBoxPlotData], invoke: () => { /*// sort data order within boxes, for rendering order (z-index) const sortedData = this._unsortedBoxPlotData.result!.map(labelAndData=>({ label: labelAndData.label, data: sortScatterPlotDataForZIndex(labelAndData.data, this.viewType) }));*/ const sortedData = this._unsortedBoxPlotData.result!; // sort box order if (this.sortBy === 'alphabetic') { return Promise.resolve( _.sortBy( sortedData, (d: IBoxScatterPlotData) => d.label ) ); } else { return Promise.resolve( _.sortBy( sortedData, (d: IBoxScatterPlotData) => { //Note: we have to use slice to convert Seamless immutable array to real array, otherwise jStat chokes return jStat.median( Array.prototype.slice.apply( d.data.map( (v: IBoxScatterPlotPoint) => v.value as number ) ) ); } ) ); } }, }); @computed get studyTypeCounts() { const allStudies = _.values(this.props.studyMap); return { provisional: allStudies.filter(study => isTCGAProvStudy(study.studyId) ), panCancer: allStudies.filter(study => isPanCanStudy(study.studyId)), }; } @autobind handleStudySelection(event: React.SyntheticEvent) { // toggle state of it this.selectedStudyIds[event.currentTarget.value] = !this .selectedStudyIds[event.currentTarget.value]; } @autobind handleRNASeqVersionChange(event: React.SyntheticEvent) { this._rnaSeqVersion = event.currentTarget.value as any; } @autobind handleSortByChange(event: React.SyntheticEvent) { this.sortBy = event.currentTarget.value as any; } @autobind handleSelectAllStudies() { if (this.studiesWithExpressionData.isComplete) { this.applyStudyFilter(study => { return study.studyId in this.studiesWithExpressionData.result!; }); } } @autobind handleDeselectAllStudies() { this.applyStudyFilter(study => { return false; }); } @computed get hasUnselectedStudies() { if (this.studiesWithExpressionData.isComplete) { return ( undefined !== _.find(this.props.studyMap, study => { const hasData = study.studyId in this.studiesWithExpressionData.result!; const isSelected = this.selectedStudies.includes(study); return hasData && !isSelected; }) ); } else { return false; } } @computed get alphabetizedAndFilteredStudies() { return _.chain(this.props.studyMap) .values() .filter( (study: CancerStudy) => study.studyId in (this.studiesWithExpressionData.result || {}) ) .sortBy((study: CancerStudy) => study.name) .value(); } @computed get studySelectionModal() { return ( { this.studySelectorModalVisible = false; }} className="cbioportal-frontend" > Select Studies
{_.map( this.alphabetizedAndFilteredStudies, (study: CancerStudy) => { return (
); } )}
); } @autobind public applyStudyFilter(test: (study: CancerStudy) => boolean) { this.selectedStudyIds = _.mapValues( this.props.studyMap, (study: CancerStudy, studyId: string) => { return test(study); } ); } @autobind public handleTabClick(id: string, datum: Gene) { this.selectedGene = datum; } @computed get yAxisLabel() { const log = this.logScale ? ' (log2)' : ''; let rnaSeqVersion = '1'; if (this.selectedRNASeqVersion === 'rna_seq_v2_mrna') { rnaSeqVersion = '2'; } return `${this.selectedGene.hugoGeneSymbol} Expression --- RNA Seq V${rnaSeqVersion}${log}`; } @computed get fill() { if (this.showCna || this.showMutations) { return (d: IPlotSampleData) => this.scatterPlotAppearance(d).fill!; } else { return basicAppearance.fill; } } @computed get fillOpacity() { if (!this.showMutations && this.showCna) { // no fill for CNA mode return 0; } else { return 0.7; } } @computed get strokeWidth() { if (this.showCna) { return CNA_STROKE_WIDTH; } else { return 1; } } @autobind private strokeOpacity(d: IPlotSampleData) { return this.scatterPlotAppearance(d).strokeOpacity; } @autobind private stroke(d: IPlotSampleData) { return this.scatterPlotAppearance(d).stroke; } @computed get scatterPlotAppearance() { return makeScatterPlotPointAppearance( this.coloringTypes, this.mutationDataExists.result!, this.cnaDataExists.result!, false, this.props.store.driverAnnotationSettings.driversAnnotated ); } @computed get coloringTypes() { const ret: SelectedColoringTypes = {}; if (this.showMutations) { ret[ColoringType.MutationType] = true; } if (this.showCna) { ret[ColoringType.CopyNumber] = true; } return ret; } private boxCalculationFilter(d: IBoxScatterPlotPoint) { // filter out zero values from box calculation return d.value !== 0; } @autobind private tooltip(d: IBoxScatterPlotPoint) { let content; if (this.props.store.studyIdToStudy.isComplete) { content = expressionTooltip( d, this.props.store.studyIdToStudy.result! ); } else { content = Loading...; } return content; } @computed get zIndexSortBy() { return scatterPlotZIndexSortBy(this.coloringTypes); } @computed get axisLogScaleFunction(): IAxisLogScaleParams | undefined { const MIN_LOG_ARGUMENT = 0.01; if (!this.logScale) { return undefined; } return { label: 'log2', fLogScale: (x: number, offset: number) => Math.log2(Math.max(x, MIN_LOG_ARGUMENT)), fInvLogScale: (x: number) => Math.pow(2, x), }; } @autobind private getChart() { if ( this.boxPlotData.isComplete && this.mutationDataExists.isComplete && this.cnaDataExists.isComplete ) { return ( d.data)), this.coloringTypes, PlotType.BoxPlot, this.props.store.driverAnnotationSettings .driversAnnotated, [] )} legendLocationWidthThreshold={900} boxCalculationFilter={this.boxCalculationFilter} /> ); } else { return ; } } @autobind private getSvg() { return document.getElementById(SVG_ID) as SVGElement | null; } @computed get exportFileName() { // TODO: more specific? return 'Expression'; } render() { return (
{this.studySelectorModalVisible && this.studySelectionModal}
{this.props.genes.map((gene: Gene) => { return ( ); })}
Profile:
{this.boxPlotData.isComplete && this.boxPlotData.result.length > 1 && (
Sort By:
)}
{this.mutationDataExists.result && ( )} {this.cnaDataExists.result && ( )}
{this.studiesWithExpressionData.isComplete && (
  0 } > 0 } >
)}
{this.boxPlotData.isComplete && ( 0}> 0} >
{this.getChart} {this.mutationDataExists.result && (
* Driver annotation settings are located in the Mutation Color menu of the Oncoprint.
)}
No expression data for this query.
No studies selected.
)}
); } }