diff --git a/src/pages/resultsView/ResultsViewPageStore.ts b/src/pages/resultsView/ResultsViewPageStore.ts index 8f75b241bba..d0d1892ff80 100644 --- a/src/pages/resultsView/ResultsViewPageStore.ts +++ b/src/pages/resultsView/ResultsViewPageStore.ts @@ -304,6 +304,7 @@ import { ONCOKB_DEFAULT_INFO, USE_DEFAULT_PUBLIC_INSTANCE_FOR_ONCOKB, } from 'react-mutation-mapper'; +import { RGBAColor } from 'oncoprintjs'; type Optional = | { isApplicable: true; value: T } @@ -573,6 +574,15 @@ export class ResultsViewPageStore extends AnalysisStore @observable queryFormVisible: boolean = false; + @observable userAlterationColors: { + [alteration: string]: string | undefined; + } = {}; + + private _selectedComparisonGroupsWarningSigns = observable.map< + string, + boolean + >({}, { deep: false }); + @computed get doNonSelectedDownloadableMolecularProfilesExist() { return ( this.nonSelectedDownloadableMolecularProfilesGroupByName.result && @@ -586,6 +596,52 @@ export class ResultsViewPageStore extends AnalysisStore | ModifyQueryParams | undefined = undefined; + @action.bound + public onAlterationColorChange( + alteration: string, + color: string | undefined + ) { + if (color == undefined && this.userAlterationColors[alteration]) { + delete this.userAlterationColors[alteration]; + } else this.userAlterationColors[alteration] = color; + } + + @action public showAlterationWarningSign( + alteration: string, + markedValue: boolean + ) { + this._selectedComparisonGroupsWarningSigns.set(alteration, markedValue); + } + + public flagDuplicateColorsForAlterations( + alteration: string, + color: string | undefined + ) { + let colors: { [color: string]: number } = {}; + + Object.keys(this.userAlterationColors).forEach( + (a: string, i: number) => { + let alterationColor = + a === alteration ? color : this.userAlterationColors[a]; + if ( + alterationColor == undefined || + colors[alterationColor] == undefined + ) { + if (alterationColor != undefined) + colors[alterationColor] = 1; + this.showAlterationWarningSign(alteration, false); + } else { + colors[alterationColor] = colors[alterationColor] + 1; + this.showAlterationWarningSign(alteration, true); + } + } + ); + } + + public isAlterationMarkedWithWarningSign(alteration: string): boolean { + return !!this._selectedComparisonGroupsWarningSigns.get(alteration); + } + @action.bound public setOncoprintAnalysisCaseType(e: OncoprintAnalysisCaseType) { this.urlWrapper.updateURL({ diff --git a/src/shared/components/oncoprint/DeltaUtils.ts b/src/shared/components/oncoprint/DeltaUtils.ts index 0a66fe42f7e..4a1e82a6f37 100644 --- a/src/shared/components/oncoprint/DeltaUtils.ts +++ b/src/shared/components/oncoprint/DeltaUtils.ts @@ -15,6 +15,7 @@ import { SortConfig, TrackId, UserTrackSpec, + IGeneticAlterationRuleSetParams, } from 'oncoprintjs'; import _ from 'lodash'; import { @@ -55,7 +56,8 @@ export function transition( getTrackSpecKeyToTrackId: () => { [key: string]: TrackId }, getMolecularProfileMap: () => | { [molecularProfileId: string]: MolecularProfile } - | undefined + | undefined, + customRule?: IGeneticAlterationRuleSetParams ) { const notKeepingSorted = shouldNotKeepSortedForTransition( nextProps, @@ -85,7 +87,8 @@ export function transition( prevProps, oncoprint, getTrackSpecKeyToTrackId, - getMolecularProfileMap + getMolecularProfileMap, + customRule ); transitionSortConfig(nextProps, prevProps, oncoprint); transitionTrackGroupSortPriority(nextProps, prevProps, oncoprint); @@ -582,7 +585,8 @@ function hasGeneticTrackRuleSetChanged( prevProps.distinguishMutationType || nextProps.distinguishDrivers !== prevProps.distinguishDrivers || nextProps.distinguishGermlineMutations !== - prevProps.distinguishGermlineMutations + prevProps.distinguishGermlineMutations || + nextProps.test !== prevProps.test ); } @@ -593,7 +597,8 @@ function transitionTracks( getTrackSpecKeyToTrackId: () => { [key: string]: TrackId }, getMolecularProfileMap: () => | { [molecularProfileId: string]: MolecularProfile } - | undefined + | undefined, + customRule?: IGeneticAlterationRuleSetParams ) { // Initialize tracks for rule set sharing const trackIdForRuleSetSharing = { @@ -735,7 +740,9 @@ function transitionTracks( oncoprint, nextProps, prevProps, - trackIdForRuleSetSharing + trackIdForRuleSetSharing, + undefined, + customRule ); delete prevGeneticTracks[track.key]; } @@ -750,7 +757,9 @@ function transitionTracks( oncoprint, nextProps, prevProps, - trackIdForRuleSetSharing + trackIdForRuleSetSharing, + undefined, + customRule ); } } @@ -1088,20 +1097,29 @@ function transitionGeneticTrack( nextProps: IOncoprintProps, prevProps: Partial, trackIdForRuleSetSharing: { genetic?: TrackId }, - expansionParentKey?: string + expansionParentKey?: string, + customRule?: IGeneticAlterationRuleSetParams ) { const trackSpecKeyToTrackId = getTrackSpecKeyToTrackId(); if (tryRemoveTrack(nextSpec, prevSpec, trackSpecKeyToTrackId, oncoprint)) { // Remove track return; } else if (nextSpec && !prevSpec) { + let rule = customRule + ? customRule + : getGeneticTrackRuleSetParams( + nextProps.distinguishMutationType, + nextProps.distinguishDrivers, + nextProps.distinguishGermlineMutations + ); + console.log(rule); + // if (rule.rule_params.conditional.disp_mut.inframe) { + // console.log('hi') + // rule.rule_params.conditional.disp_mut.inframe.shapes[0].fill = [153, 52, 4, 1] + // } // Add track const geneticTrackParams: UserTrackSpec = { - rule_set_params: getGeneticTrackRuleSetParams( - nextProps.distinguishMutationType, - nextProps.distinguishDrivers, - nextProps.distinguishGermlineMutations - ), + rule_set_params: rule, label: nextSpec.label, sublabel: nextSpec.sublabel, track_label_color: nextSpec.labelColor || undefined, @@ -1197,15 +1215,16 @@ function transitionGeneticTrack( trackId ); } else { + let rule = customRule + ? customRule + : getGeneticTrackRuleSetParams( + nextProps.distinguishMutationType, + nextProps.distinguishDrivers, + nextProps.distinguishGermlineMutations + ); + console.log(rule); // otherwise, update ruleset - oncoprint.setRuleSet( - trackId, - getGeneticTrackRuleSetParams( - nextProps.distinguishMutationType, - nextProps.distinguishDrivers, - nextProps.distinguishGermlineMutations - ) - ); + oncoprint.setRuleSet(trackId, rule); } } // either way, use this one now diff --git a/src/shared/components/oncoprint/Oncoprint.tsx b/src/shared/components/oncoprint/Oncoprint.tsx index 25a7fd6059d..95e82776165 100644 --- a/src/shared/components/oncoprint/Oncoprint.tsx +++ b/src/shared/components/oncoprint/Oncoprint.tsx @@ -7,6 +7,7 @@ import { TrackSortDirection, InitParams, ColumnLabel, + IGeneticAlterationRuleSetParams, } from 'oncoprintjs'; import { GenePanelData, MolecularProfile } from 'cbioportal-ts-api-client'; import { observer } from 'mobx-react'; @@ -275,6 +276,8 @@ export interface IOncoprintProps { alterationTypesInQuery?: string[]; distinguishMutationType?: boolean; + test?: boolean; + rule?: IGeneticAlterationRuleSetParams; distinguishDrivers?: boolean; distinguishGermlineMutations?: boolean; @@ -386,7 +389,8 @@ export default class Oncoprint extends React.Component { () => this.trackSpecKeyToTrackId, () => { return this.props.molecularProfileIdToMolecularProfile; - } + }, + this.props.rule ); this.lastTransitionProps = _.clone(props); } diff --git a/src/shared/components/oncoprint/ResultsViewOncoprint.tsx b/src/shared/components/oncoprint/ResultsViewOncoprint.tsx index e0b2af3879f..f24f4f33bb1 100644 --- a/src/shared/components/oncoprint/ResultsViewOncoprint.tsx +++ b/src/shared/components/oncoprint/ResultsViewOncoprint.tsx @@ -41,6 +41,7 @@ import { AlterationTypeConstants } from 'shared/constants'; import { ResultsViewPageStore } from '../../../pages/resultsView/ResultsViewPageStore'; import { getAlteredUids, + getGeneticTrackRuleSetParams, getUnalteredUids, makeClinicalTracksMobxPromise, makeGenericAssayProfileCategoricalTracksMobxPromise, @@ -54,7 +55,13 @@ import _ from 'lodash'; import { onMobxPromise, toPromise } from 'cbioportal-frontend-commons'; import { getServerConfig } from 'config/config'; import LoadingIndicator from 'shared/components/loadingIndicator/LoadingIndicator'; -import { OncoprintJS, TrackGroupIndex, TrackId } from 'oncoprintjs'; +import { + IGeneticAlterationRuleSetParams, + OncoprintJS, + RGBAColor, + TrackGroupIndex, + TrackId, +} from 'oncoprintjs'; import fileDownload from 'react-file-download'; import tabularDownload from './tabularDownload'; import classNames from 'classnames'; @@ -89,6 +96,7 @@ import '../../../globalStyles/oncoprintStyles.scss'; import { GenericAssayTrackInfo } from 'pages/studyView/addChartButton/genericAssaySelection/GenericAssaySelection'; import { toDirectionString } from './SortUtils'; import { RestoreClinicalTracksMenu } from 'pages/resultsView/oncoprint/RestoreClinicalTracksMenu'; +import { hexToRGBA } from 'shared/lib/Colors'; interface IResultsViewOncoprintProps { divId: string; @@ -253,6 +261,15 @@ export default class ResultsViewOncoprint extends React.Component< @observable renderingComplete = false; + @observable + rule: IGeneticAlterationRuleSetParams = getGeneticTrackRuleSetParams( + this.distinguishMutationType, + this.distinguishDrivers, + this.distinguishGermlineMutations + ); + + @observable test: boolean = false; + private heatmapGeneInputValueUpdater: IReactionDisposer; private molecularProfileIdToTrackGroupIndex: { @@ -485,9 +502,15 @@ export default class ResultsViewOncoprint extends React.Component< get sortByCaseListDisabled() { return !self.caseListSortPossible; }, + get rule() { + return self.rule; + }, get distinguishMutationType() { return self.distinguishMutationType; }, + get test() { + return self.test; + }, get distinguishDrivers() { return self.distinguishDrivers; }, @@ -705,6 +728,12 @@ export default class ResultsViewOncoprint extends React.Component< onSelectDistinguishMutationType: (s: boolean) => { this.distinguishMutationType = s; }, + onSelectTest: (s: boolean) => { + this.test = s; + }, + onSetRule: (rule: IGeneticAlterationRuleSetParams) => { + this.rule = rule; + }, onSelectDistinguishDrivers: action((s: boolean) => { if (!s) { this.props.store.driverAnnotationSettings.oncoKb = false; @@ -1690,6 +1719,7 @@ export default class ResultsViewOncoprint extends React.Component< this .selectedGenericAssayEntitiesGroupedByGenericAssayTypeFromUrl } + setRules={this.setRules} /> ); @@ -1792,6 +1822,53 @@ export default class ResultsViewOncoprint extends React.Component< return WindowStore.size.width - 75; } + @action.bound + public setRules(alteration: string, color: RGBAColor | undefined) { + if (color == undefined) { + this.rule = getGeneticTrackRuleSetParams( + this.distinguishMutationType, + this.distinguishDrivers, + this.distinguishGermlineMutations + ); + for (alteration in this.props.store.userAlterationColors) { + if ( + this.props.store.userAlterationColors[alteration] !== + undefined + ) { + this.rule.rule_params.conditional.disp_mut[ + alteration + ].shapes[0].fill = hexToRGBA( + this.props.store.userAlterationColors[alteration]! + ); + } + } + } else { + // const rules = getGeneticTrackRuleSetParams(this.distinguishMutationType, + // this.distinguishDrivers, + // this.distinguishGermlineMutations + // ); + // if (rules.rule_params.conditional.disp_mut.missense && this.test) { + // rules.rule_params.conditional.disp_mut.missense.shapes[0].fill = [0, 128, 0, 1] + // } + // else if (rules.rule_params.conditional.disp_mut.missense && !this.test) { + // rules.rule_params.conditional.disp_mut.missense.shapes[0].fill = [83, 212, 0, 1] + // } + if (this.rule.rule_params.conditional.disp_mut[alteration]) { + this.rule.rule_params.conditional.disp_mut[ + alteration + ].shapes[0].fill = color; + } + // else if (rules.rule_params.conditional.disp_mut['splice,missense,inframe,trunc,promoter,other'] && this.test) { + // rules.rule_params.conditional.disp_mut['splice,missense,inframe,trunc,promoter,other'].shapes[0].fill = [0, 128, 0, 1] + // } + // else if (rules.rule_params.conditional.disp_mut['splice,missense,inframe,trunc,promoter,other'] && !this.test) { + // rules.rule_params.conditional.disp_mut['splice,missense,inframe,trunc,promoter,other'].shapes[0].fill = [83, 212, 0, 1] + // } + // this.rule = rules; + } + console.log(this.rule); + } + public render() { return (
@@ -1892,6 +1969,7 @@ export default class ResultsViewOncoprint extends React.Component< distinguishMutationType={ this.distinguishMutationType } + test={this.test} distinguishDrivers={this.distinguishDrivers} distinguishGermlineMutations={ this.distinguishGermlineMutations @@ -1915,6 +1993,7 @@ export default class ResultsViewOncoprint extends React.Component< initParams={{ max_height: Number.POSITIVE_INFINITY, }} + rule={this.rule} />
diff --git a/src/shared/components/oncoprint/controls/OncoprintColors.tsx b/src/shared/components/oncoprint/controls/OncoprintColors.tsx new file mode 100644 index 00000000000..8cd23a2c4eb --- /dev/null +++ b/src/shared/components/oncoprint/controls/OncoprintColors.tsx @@ -0,0 +1,181 @@ +import * as React from 'react'; +import { observer } from 'mobx-react'; +import autobind from 'autobind-decorator'; +import { action, computed, makeObservable, observable } from 'mobx'; +import styles from '../styles.module.scss'; +import { CirclePicker, CirclePickerProps } from 'react-color'; +import { OverlayTrigger, Popover } from 'react-bootstrap'; +import { DefaultTooltip } from 'cbioportal-frontend-commons'; +import { ColorPickerIcon } from 'pages/groupComparison/comparisonGroupManager/ColorPickerIcon'; +import { + CLI_FEMALE_COLOR, + CLI_NO_COLOR, + CLI_YES_COLOR, + DARK_GREY, +} from 'shared/lib/Colors'; +import { COLORS } from 'pages/studyView/StudyViewUtils'; +import { ResultsViewPageStore } from 'pages/resultsView/ResultsViewPageStore'; +import { + IOncoprintControlsHandlers, + IOncoprintControlsState, +} from './OncoprintControls'; +import { RGBAColor } from 'oncoprintjs'; + +export interface IGroupCheckboxProps { + alteration: string; + store?: ResultsViewPageStore; + handlers: IOncoprintControlsHandlers; + state: IOncoprintControlsState; + setRules?: (alteration: string, color: RGBAColor | undefined) => void; + color?: string; + markedWithWarningSign: boolean; +} + +const COLOR_UNDEFINED = '#FFFFFF'; + +const alterationToLabel: { [alteration: string]: string } = { + missense: 'Missense Mutation (unknown significance)', + missense_rec: 'Missense Mutation (putative driver)', + trunc: 'Truncating Mutation (unknown significance)', + trunc_rec: 'Truncating Mutation (putative driver)', + inframe: 'Inframe Mutation (unknown significance)', + inframe_rec: 'Inframe Mutation (putative driver)', + splice: 'Splice Mutation (unknown significance)', + splice_rec: 'Splice Mutation (putative driver)', + promoter: 'Promoter Mutation (unknown significance)', + promoter_rec: 'Promoter Mutation (putative driver)', + other: 'Other Mutation (unknown significance)', + other_rec: 'Other Mutation (putative driver)', +}; + +@observer +export default class OncoprintColors extends React.Component< + IGroupCheckboxProps, + {} +> { + constructor(props: IGroupCheckboxProps) { + super(props); + makeObservable(this); + } + + @action.bound + handleChangeComplete = (color: any, event: any) => { + // if same color is select, unselect it (go back to no color) + if (color.hex === this.props.color) { + this.props.store?.onAlterationColorChange( + this.props.alteration, + undefined + ); + this.props.setRules && + this.props.setRules(this.props.alteration, undefined); + this.props.store!.flagDuplicateColorsForAlterations( + this.props.alteration, + undefined + ); + } else { + this.props.store?.onAlterationColorChange( + this.props.alteration, + color.hex + ); + this.props.setRules && + this.props.setRules(this.props.alteration, [ + color.rgb.r, + color.rgb.g, + color.rgb.b, + color.rgb.a, + ]); + this.props.store!.flagDuplicateColorsForAlterations( + this.props.alteration, + color.hex + ); + } + this.props.handlers.onSelectTest && + this.props.handlers.onSelectTest(!this.props.state.test!); + this.props.handlers.onSetRule && + this.props.handlers.onSetRule(this.props.state.rule!); + }; + + @computed get colorList() { + let colors: string[] = COLORS.slice(0, 20); + colors.push(CLI_YES_COLOR); + colors.push(CLI_NO_COLOR); + colors.push(CLI_FEMALE_COLOR); + colors.push(DARK_GREY); + return colors; + } + + buildColorChooserWidget = () => ( + +
{ + e.nativeEvent.stopImmediatePropagation(); + }} + onClick={e => { + e.nativeEvent.stopImmediatePropagation(); + }} + > + +
+
+ ); + + render() { + return ( +
+ {alterationToLabel[this.props.alteration]} + + + {this.props.markedWithWarningSign && ( + + )} + + + + + + + + + +
+ ); + } +} diff --git a/src/shared/components/oncoprint/controls/OncoprintControls.tsx b/src/shared/components/oncoprint/controls/OncoprintControls.tsx index f08855e191a..d31bb002a55 100644 --- a/src/shared/components/oncoprint/controls/OncoprintControls.tsx +++ b/src/shared/components/oncoprint/controls/OncoprintControls.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { observer } from 'mobx-react'; -import { Button, ButtonGroup } from 'react-bootstrap'; +import { Button, ButtonGroup, OverlayTrigger, Popover } from 'react-bootstrap'; import CustomDropdown from './CustomDropdown'; import ConfirmNgchmModal from './ConfirmNgchmModal'; import ReactSelect from 'react-select1'; @@ -32,7 +32,9 @@ import { import OQLTextArea, { GeneBoxType } from '../../GeneSelectionBox/OQLTextArea'; import autobind from 'autobind-decorator'; import { SingleGeneQuery } from '../../../lib/oql/oql-parser'; -import TracksMenu from 'pages/resultsView/oncoprint/TracksMenu'; +import TracksMenu, { + MIN_DROPDOWN_WIDTH, +} from 'pages/resultsView/oncoprint/TracksMenu'; import { GenericAssayTrackInfo } from 'pages/studyView/addChartButton/genericAssaySelection/GenericAssaySelection'; import { IDriverAnnotationControlsHandlers, @@ -44,6 +46,8 @@ import { ClinicalTrackConfigMap, } from 'shared/components/oncoprint/Oncoprint'; import { getServerConfig } from 'config/config'; +import { IGeneticAlterationRuleSetParams, RGBAColor } from 'oncoprintjs'; +import OncoprintColors from './OncoprintColors'; export interface IOncoprintControlsHandlers extends IDriverAnnotationControlsHandlers { @@ -58,6 +62,8 @@ export interface IOncoprintControlsHandlers onSelectShowMinimap: (showMinimap: boolean) => void; onSelectDistinguishMutationType: (distinguish: boolean) => void; onSelectDistinguishGermlineMutations: (distinguish: boolean) => void; + onSelectTest?: (distinguish: boolean) => void; + onSetRule?: (rule: IGeneticAlterationRuleSetParams) => void; onSelectHideVUS: (hide: boolean) => void; onSelectHideGermlineMutations: (hide: boolean) => void; @@ -101,6 +107,8 @@ export interface IOncoprintControlsState sortByCaseListDisabled: boolean; hidePutativePassengers: boolean; hideGermlineMutations: boolean; + test?: boolean; + rule?: IGeneticAlterationRuleSetParams; sortMode?: SortMode; clinicalAttributesPromise?: MobxPromise; @@ -136,6 +144,7 @@ export interface IOncoprintControlsProps { selectedGenericAssayEntitiesGroupedByGenericAssayTypeFromUrl?: { [genericAssayType: string]: string[]; }; + setRules?: (alteration: string, color: RGBAColor | undefined) => void; } export interface ISelectOption { @@ -178,6 +187,8 @@ const EVENT_KEY = { downloadOncoprinter: '29.1', horzZoomSlider: '30', viewNGCHM: '31', + test: '32', + rule: '33', }; @observer @@ -319,7 +330,23 @@ export default class OncoprintControls extends React.Component< this.props.handlers.onSelectDistinguishMutationType( !this.props.state.distinguishMutationType ); + // this.props.test && this.props.test(); + // this.props.handlers.onSetRule && + // this.props.handlers.onSetRule( + // this.props.state.rule + // ); break; + // case EVENT_KEY.rule: + // this.props.handlers.onSelectTest && + // this.props.handlers.onSelectTest( + // !this.props.state.test + // ); + // this.props.test && this.props.test(); + // this.props.handlers.onSetRule && + // this.props.handlers.onSetRule( + // this.props.state.rule + // ); + // break; case EVENT_KEY.distinguishGermlineMutations: this.props.handlers.onSelectDistinguishGermlineMutations( !this.props.state.distinguishGermlineMutations @@ -830,6 +857,70 @@ export default class OncoprintControls extends React.Component< } } + private ColorsMenuOncoprint = observer(() => { + return ( + +
+ {[ + 'missense', + 'missense_rec', + 'trunc', + 'trunc_rec', + 'inframe', + 'inframe_rec', + 'splice', + 'splice_rec', + 'promoter', + 'promoter_rec', + 'other', + 'other_rec', + ].map(alteration => ( + + ))} + {/*
Color by Alteration
+
+
+ +
+
*/} +
+
+ ); + }); + private MutationColorMenu = observer(() => { return ( +