diff --git a/end-to-end-test/remote/screenshots/reference/oncoprint_reflects_default_colors_element_chrome_1600x1000.png b/end-to-end-test/remote/screenshots/reference/oncoprint_reflects_default_colors_element_chrome_1600x1000.png new file mode 100644 index 00000000000..299bab34206 Binary files /dev/null and b/end-to-end-test/remote/screenshots/reference/oncoprint_reflects_default_colors_element_chrome_1600x1000.png differ diff --git a/end-to-end-test/remote/screenshots/reference/oncoprint_reflects_user_selected_colors_element_chrome_1600x1000.png b/end-to-end-test/remote/screenshots/reference/oncoprint_reflects_user_selected_colors_element_chrome_1600x1000.png new file mode 100644 index 00000000000..097e4a73638 Binary files /dev/null and b/end-to-end-test/remote/screenshots/reference/oncoprint_reflects_user_selected_colors_element_chrome_1600x1000.png differ diff --git a/end-to-end-test/remote/screenshots/reference/oncoprint_uses_default_background_for_glyphs_when_option_not_toggled_element_chrome_1600x1000.png b/end-to-end-test/remote/screenshots/reference/oncoprint_uses_default_background_for_glyphs_when_option_not_toggled_element_chrome_1600x1000.png new file mode 100644 index 00000000000..5f3562192b7 Binary files /dev/null and b/end-to-end-test/remote/screenshots/reference/oncoprint_uses_default_background_for_glyphs_when_option_not_toggled_element_chrome_1600x1000.png differ diff --git a/end-to-end-test/remote/screenshots/reference/oncoprint_uses_white_background_for_glyphs_when_option_toggled_element_chrome_1600x1000.png b/end-to-end-test/remote/screenshots/reference/oncoprint_uses_white_background_for_glyphs_when_option_toggled_element_chrome_1600x1000.png new file mode 100644 index 00000000000..979e6706601 Binary files /dev/null and b/end-to-end-test/remote/screenshots/reference/oncoprint_uses_white_background_for_glyphs_when_option_toggled_element_chrome_1600x1000.png differ diff --git a/end-to-end-test/remote/specs/core/resultsOncoprintColorConfig.spec.js b/end-to-end-test/remote/specs/core/resultsOncoprintColorConfig.spec.js new file mode 100644 index 00000000000..27368e2d1d5 --- /dev/null +++ b/end-to-end-test/remote/specs/core/resultsOncoprintColorConfig.spec.js @@ -0,0 +1,190 @@ +var assertScreenShotMatch = require('../../../shared/lib/testUtils') + .assertScreenShotMatch; +var assert = require('assert'); +var waitForOncoprint = require('../../../shared/specUtils').waitForOncoprint; +var goToUrlAndSetLocalStorage = require('../../../shared/specUtils') + .goToUrlAndSetLocalStorage; +var getNthOncoprintTrackOptionsElements = require('../../../shared/specUtils') + .getNthOncoprintTrackOptionsElements; +var getTextInOncoprintLegend = require('../../../shared/specUtils') + .getTextInOncoprintLegend; +var { + checkOncoprintElement, + getElementByTestHandle, +} = require('../../../shared/specUtils.js'); + +const ONCOPRINT_TIMEOUT = 60000; +const CBIOPORTAL_URL = process.env.CBIOPORTAL_URL.replace(/\/$/, ''); + +describe('oncoprint colors', () => { + describe('clinical tracks color configuration', () => { + before(function() { + goToUrlAndSetLocalStorage( + `${CBIOPORTAL_URL}/results/oncoprint?Action=Submit&RPPA_SCORE_THRESHOLD=2.0&Z_SCORE_THRESHOLD=2.0&cancer_study_list=gbm_tcga&case_set_id=gbm_tcga_all&data_priority=0&gene_list=EGFR%250APTEN%250AIDH1%250ATP53&geneset_list=%20&genetic_profile_ids_PROFILE_COPY_NUMBER_ALTERATION=gbm_tcga_gistic&genetic_profile_ids_PROFILE_MRNA_EXPRESSION=gbm_tcga_mrna_median_all_sample_Zscores&genetic_profile_ids_PROFILE_MUTATION_EXTENDED=gbm_tcga_mutations&hide_unprofiled_samples=false&profileFilter=0&tab_index=tab_visualize&show_samples=false` + ); + waitForOncoprint(ONCOPRINT_TIMEOUT); + }); + + it('color configuration modal reflects user selected colors', () => { + // add "Mutation spectrum" track + const $tracksDropdown = $('#addTracksDropdown'); + $tracksDropdown.click(); + getElementByTestHandle( + 'add-chart-option-mutation-spectrum' + ).waitForDisplayed(); + getElementByTestHandle('add-chart-option-mutation-spectrum') + .$('label') + .click(); + waitForOncoprint(ONCOPRINT_TIMEOUT); + + // check that mutation spectrum is added to the oncoprint + let legendText = getTextInOncoprintLegend(); + assert(legendText.indexOf('Mutation spectrum') > -1); + + $tracksDropdown.waitForDisplayed(); + $tracksDropdown.click(); + + var trackOptionsElts = getNthOncoprintTrackOptionsElements(5); + // open menu + $(trackOptionsElts.button_selector).click(); + $(trackOptionsElts.dropdown_selector).waitForDisplayed({ + timeout: 1000, + }); + // click "Edit Colors" to open modal + $(trackOptionsElts.dropdown_selector + ' li:nth-child(8)').click(); + browser.pause(1000); + + // select new colors for track values + getElementByTestHandle('color-picker-icon').click(); + $('.circle-picker').waitForDisplayed({ timeout: 1000 }); + $('.circle-picker [title="#990099"]').click(); + waitForOncoprint(ONCOPRINT_TIMEOUT); + getElementByTestHandle('color-picker-icon').waitForDisplayed(); + getElementByTestHandle('color-picker-icon').click(); + $('.circle-picker').waitForDisplayed({ reverse: true }); + + $$('[data-test="color-picker-icon"]')[1].click(); + $('.circle-picker').waitForDisplayed({ timeout: 1000 }); + $('.circle-picker [title="#109618"]').click(); + waitForOncoprint(ONCOPRINT_TIMEOUT); + getElementByTestHandle('color-picker-icon').waitForDisplayed(); + $$('[data-test="color-picker-icon"]')[1].click(); + $('.circle-picker').waitForDisplayed({ reverse: true }); + + $$('[data-test="color-picker-icon"]')[2].click(); + $('.circle-picker').waitForDisplayed({ timeout: 1000 }); + $('.circle-picker [title="#8b0707"]').click(); + waitForOncoprint(ONCOPRINT_TIMEOUT); + + assert.strictEqual( + $('[data-test="color-picker-icon"] rect').getAttribute('fill'), + '#990099' + ); + assert.strictEqual( + $$('[data-test="color-picker-icon"] rect')[1].getAttribute( + 'fill' + ), + '#109618' + ); + assert.strictEqual( + $$('[data-test="color-picker-icon"] rect')[2].getAttribute( + 'fill' + ), + '#8b0707' + ); + }); + + it('oncoprint reflects user selected colors', () => { + // close modal + $('a.tabAnchor_oncoprint').click(); + var res = checkOncoprintElement(); + assertScreenShotMatch(res); + }); + + it('reset colors button is visible when default colors not used', () => { + // click "Edit Colors" to open modal and check "Reset Colors" button in modal + var trackOptionsElts = getNthOncoprintTrackOptionsElements(5); + $(trackOptionsElts.button_selector).click(); + $(trackOptionsElts.dropdown_selector).waitForDisplayed({ + timeout: 1000, + }); + $(trackOptionsElts.dropdown_selector + ' li:nth-child(8)').click(); + getElementByTestHandle('resetColors').waitForDisplayed(); + }); + + it('color configuration modal reflects default colors', () => { + // click "Reset Colors" track + getElementByTestHandle('resetColors').click(); + waitForOncoprint(ONCOPRINT_TIMEOUT); + + assert.strictEqual( + $('[data-test="color-picker-icon"] rect').getAttribute('fill'), + '#3d6eb1' + ); + assert.strictEqual( + $$('[data-test="color-picker-icon"] rect')[1].getAttribute( + 'fill' + ), + '#8ebfdc' + ); + assert.strictEqual( + $$('[data-test="color-picker-icon"] rect')[2].getAttribute( + 'fill' + ), + '#dff1f8' + ); + }); + + it('oncoprint reflects default colors', () => { + // close modal + $('a.tabAnchor_oncoprint').click(); + var res = checkOncoprintElement(); + assertScreenShotMatch(res); + }); + + it('reset colors button is hidden when default colors are used', () => { + // click "Edit Colors" to open modal and check "Reset Colors" button in modal + var trackOptionsElts = getNthOncoprintTrackOptionsElements(5); + $(trackOptionsElts.button_selector).click(); + $(trackOptionsElts.dropdown_selector).waitForDisplayed({ + timeout: 1000, + }); + $(trackOptionsElts.dropdown_selector + ' li:nth-child(8)').click(); + getElementByTestHandle('resetColors').waitForDisplayed({ + reverse: true, + }); + }); + }); + + describe('enable white background for glyphs', () => { + before(function() { + goToUrlAndSetLocalStorage( + `${CBIOPORTAL_URL}/results/oncoprint?Action=Submit&RPPA_SCORE_THRESHOLD=2.0&Z_SCORE_THRESHOLD=2.0&cancer_study_list=gbm_tcga&case_set_id=gbm_tcga_all&data_priority=0&gene_list=EGFR%250APTEN%250AIDH1%250ATP53&geneset_list=%20&genetic_profile_ids_PROFILE_COPY_NUMBER_ALTERATION=gbm_tcga_gistic&genetic_profile_ids_PROFILE_MRNA_EXPRESSION=gbm_tcga_mrna_median_all_sample_Zscores&genetic_profile_ids_PROFILE_MUTATION_EXTENDED=gbm_tcga_mutations&hide_unprofiled_samples=false&profileFilter=0&tab_index=tab_visualize&show_samples=false` + ); + waitForOncoprint(ONCOPRINT_TIMEOUT); + }); + it('oncoprint uses white background for glyphs when option toggled', () => { + // toggle on white backgrounds for glyphs + const $viewDropdown = $('#viewDropdownButton'); + $viewDropdown.click(); + waitForOncoprint(2000); + getElementByTestHandle('toggleWhiteBackgroundForGlyphs').click(); + $viewDropdown.click(); + + var res = checkOncoprintElement(); + assertScreenShotMatch(res); + }); + + it('oncoprint uses default background for glyphs when option not toggled', () => { + // toggle off white backgrounds for glyphs + const $viewDropdown = $('#viewDropdownButton'); + $viewDropdown.click(); + waitForOncoprint(2000); + getElementByTestHandle('toggleWhiteBackgroundForGlyphs').click(); + $viewDropdown.click(); + + var res = checkOncoprintElement(); + assertScreenShotMatch(res); + }); + }); +}); diff --git a/src/pages/resultsView/ResultsViewPageStore.ts b/src/pages/resultsView/ResultsViewPageStore.ts index 42e6e8f635f..4c154922fcb 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 } @@ -493,6 +494,15 @@ export class ResultsViewPageStore extends AnalysisStore } ) ); + + const clinicalTracksColorConfig = localStorage.getItem( + 'clinicalTracksColorConfig' + ); + if (clinicalTracksColorConfig !== null) { + this._userSelectedStudiesToClinicalTracksColors = JSON.parse( + clinicalTracksColorConfig + ); + } } destroy() { @@ -525,6 +535,10 @@ export class ResultsViewPageStore extends AnalysisStore get cancerStudyIds() { return this.urlWrapper.query.cancer_study_list.split(','); } + @computed + get cancerStudyListSorted() { + return this.cancerStudyIds.sort().join(','); + } @computed get rppaScoreThreshold() { @@ -573,6 +587,14 @@ export class ResultsViewPageStore extends AnalysisStore @observable queryFormVisible: boolean = false; + @observable _userSelectedStudiesToClinicalTracksColors: { + [studies: string]: { + [label: string]: { + [value: string]: RGBAColor; + }; + }; + } = { global: {} }; + @computed get doNonSelectedDownloadableMolecularProfilesExist() { return ( this.nonSelectedDownloadableMolecularProfilesGroupByName.result && @@ -586,6 +608,48 @@ export class ResultsViewPageStore extends AnalysisStore | ModifyQueryParams | undefined = undefined; + @action.bound + public setUserSelectedClinicalTrackColor( + label: string, + value: string, + color: RGBAColor | undefined + ) { + // if color is undefined, delete color from userSelectedClinicalAttributeColors if exists + // else, set the color in userSelectedClinicalAttributeColors + if ( + !color && + this._userSelectedStudiesToClinicalTracksColors['global'][label] && + this._userSelectedStudiesToClinicalTracksColors['global'][label][ + value + ] + ) { + delete this._userSelectedStudiesToClinicalTracksColors['global'][ + label + ][value]; + } else if (color) { + if ( + !this._userSelectedStudiesToClinicalTracksColors['global'][ + label + ] + ) { + this._userSelectedStudiesToClinicalTracksColors['global'][ + label + ] = {}; + } + this._userSelectedStudiesToClinicalTracksColors['global'][label][ + value + ] = color; + } + localStorage.setItem( + 'clinicalTracksColorConfig', + JSON.stringify(this._userSelectedStudiesToClinicalTracksColors) + ); + } + + @computed get userSelectedStudiesToClinicalTracksColors() { + return this._userSelectedStudiesToClinicalTracksColors; + } + @action.bound public setOncoprintAnalysisCaseType(e: OncoprintAnalysisCaseType) { this.urlWrapper.updateURL({ diff --git a/src/pages/resultsView/ResultsViewURLWrapper.ts b/src/pages/resultsView/ResultsViewURLWrapper.ts index c0a2b75d813..71fb650fa9e 100644 --- a/src/pages/resultsView/ResultsViewURLWrapper.ts +++ b/src/pages/resultsView/ResultsViewURLWrapper.ts @@ -77,6 +77,7 @@ export enum ResultsViewURLQueryEnum { geneset_list = 'geneset_list', generic_assay_groups = 'generic_assay_groups', show_samples = 'show_samples', + enable_white_background_for_glyphs = 'enable_white_background_for_glyphs', heatmap_track_groups = 'heatmap_track_groups', oncoprint_sortby = 'oncoprint_sortby', oncoprint_cluster_profile = 'oncoprint_cluster_profile', @@ -127,6 +128,7 @@ export type ResultsViewURLQuery = { const shouldForceRemount: { [prop in keyof ResultsViewURLQuery]: boolean } = { clinicallist: false, show_samples: false, + enable_white_background_for_glyphs: false, heatmap_track_groups: false, oncoprint_sortby: false, oncoprint_cluster_profile: false, @@ -181,6 +183,7 @@ const propertiesMap = _.mapValues( // oncoprint props clinicallist: { isSessionProp: false }, show_samples: { isSessionProp: false }, + enable_white_background_for_glyphs: { isSessionProp: false }, heatmap_track_groups: { isSessionProp: false }, oncoprint_sortby: { isSessionProp: false }, oncoprint_cluster_profile: { isSessionProp: false }, diff --git a/src/shared/components/oncoprint/ClinicalTrackColorPicker.tsx b/src/shared/components/oncoprint/ClinicalTrackColorPicker.tsx new file mode 100644 index 00000000000..4e211c696db --- /dev/null +++ b/src/shared/components/oncoprint/ClinicalTrackColorPicker.tsx @@ -0,0 +1,123 @@ +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, + rgbaToHex, +} from 'shared/lib/Colors'; +import { COLORS } from 'pages/studyView/StudyViewUtils'; +import { ResultsViewPageStore } from 'pages/resultsView/ResultsViewPageStore'; +import { RGBAColor } from 'oncoprintjs'; +import _ from 'lodash'; + +export interface IGroupCheckboxProps { + store: ResultsViewPageStore; + handleClinicalTrackColorChange?: ( + value: string, + color: RGBAColor | undefined + ) => void; + clinicalTrackValue: any; + color: RGBAColor; + setClinicalTrackColorChanged: (changed: boolean) => void; +} + +const COLOR_UNDEFINED = '#FFFFFF'; + +@observer +export default class ClinicalTrackColorPicker extends React.Component< + IGroupCheckboxProps, + {} +> { + constructor(props: IGroupCheckboxProps) { + super(props); + makeObservable(this); + } + + @action.bound + handleChangeComplete = (color: any, event: any) => { + // if same color is selected, unselect it (go back to default color) + if (color.hex === rgbaToHex(this.props.color)) { + this.props.handleClinicalTrackColorChange && + this.props.handleClinicalTrackColorChange( + this.props.clinicalTrackValue, + undefined + ); + } else { + this.props.handleClinicalTrackColorChange && + this.props.handleClinicalTrackColorChange( + this.props.clinicalTrackValue, + [color.rgb.r, color.rgb.g, color.rgb.b, color.rgb.a] + ); + } + // set changed track key + this.props.setClinicalTrackColorChanged(true); + }; + + @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; + } + + @computed get colorChooserElement() { + return ( + +
{ + e.nativeEvent.stopImmediatePropagation(); + }} + onClick={e => { + e.nativeEvent.stopImmediatePropagation(); + }} + > + +
+
+ ); + } + + render() { + return ( + + + + + + + + ); + } +} diff --git a/src/shared/components/oncoprint/DeltaUtils.ts b/src/shared/components/oncoprint/DeltaUtils.ts index c4225c5b9bd..f8bbffb20f8 100644 --- a/src/shared/components/oncoprint/DeltaUtils.ts +++ b/src/shared/components/oncoprint/DeltaUtils.ts @@ -587,7 +587,9 @@ function hasGeneticTrackRuleSetChanged( prevProps.distinguishMutationType || nextProps.distinguishDrivers !== prevProps.distinguishDrivers || nextProps.distinguishGermlineMutations !== - prevProps.distinguishGermlineMutations + prevProps.distinguishGermlineMutations || + nextProps.isWhiteBackgroundForGlyphsEnabled !== + prevProps.isWhiteBackgroundForGlyphsEnabled ); } @@ -1118,7 +1120,8 @@ function transitionGeneticTrack( rule_set_params: getGeneticTrackRuleSetParams( nextProps.distinguishMutationType, nextProps.distinguishDrivers, - nextProps.distinguishGermlineMutations + nextProps.distinguishGermlineMutations, + nextProps.isWhiteBackgroundForGlyphsEnabled ), label: nextSpec.label, sublabel: nextSpec.sublabel, @@ -1217,7 +1220,8 @@ function transitionGeneticTrack( getGeneticTrackRuleSetParams( nextProps.distinguishMutationType, nextProps.distinguishDrivers, - nextProps.distinguishGermlineMutations + nextProps.distinguishGermlineMutations, + nextProps.isWhiteBackgroundForGlyphsEnabled ) ); } @@ -1296,7 +1300,22 @@ function transitionClinicalTrack( target_group: CLINICAL_TRACK_GROUP_INDEX, onSortDirectionChange: nextProps.onTrackSortDirectionChange, onGapChange: nextProps.onTrackGapChange, - custom_track_options: nextSpec.custom_options, + custom_track_options: + (nextSpec.datatype === 'string' || + nextSpec.datatype === 'counts') && + nextProps.setTrackKeySelectedForEdit + ? // add edit color option that opens color config modal to custom options + [ + { + label: 'Edit Colors', + onClick: () => + nextProps.setTrackKeySelectedForEdit!( + nextSpec.key + ), + }, + ...(nextSpec.custom_options || []), + ] + : nextSpec.custom_options, track_can_show_gaps: nextSpec.datatype === 'string', show_gaps_on_init: nextSpec.gapOn, }; @@ -1323,6 +1342,22 @@ function transitionClinicalTrack( if (prevSpec.custom_options !== nextSpec.custom_options) { oncoprint.setTrackCustomOptions(trackId, nextSpec.custom_options); } + + // update ruleset if color has changed for selected track + if ( + nextProps.clinicalTrackColorChanged && + nextProps.trackKeySelectedForEdit && + getTrackSpecKeyToTrackId()[nextProps.trackKeySelectedForEdit] === + trackId + ) { + let rule_set_params = getClinicalTrackRuleSetParams(nextSpec); + rule_set_params.legend_label = nextSpec.label; + rule_set_params.exclude_from_legend = !nextProps.showClinicalTrackLegends; + rule_set_params.na_legend_label = nextSpec.na_legend_label; + oncoprint.setRuleSet(trackId, rule_set_params); + nextProps.setClinicalTrackColorChanged && + nextProps.setClinicalTrackColorChanged(false); + } } } function transitionGenesetHeatmapTrack( diff --git a/src/shared/components/oncoprint/Oncoprint.tsx b/src/shared/components/oncoprint/Oncoprint.tsx index 25a7fd6059d..52dbebbfe13 100644 --- a/src/shared/components/oncoprint/Oncoprint.tsx +++ b/src/shared/components/oncoprint/Oncoprint.tsx @@ -291,6 +291,7 @@ export interface IOncoprintProps { }; showClinicalTrackLegends?: boolean; showWhitespaceBetweenColumns?: boolean; + isWhiteBackgroundForGlyphsEnabled?: boolean; showMinimap?: boolean; onMinimapClose?: () => void; @@ -298,6 +299,11 @@ export interface IOncoprintProps { onTrackSortDirectionChange?: (trackId: TrackId, dir: number) => void; onTrackGapChange?: (trackId: TrackId, gap: boolean) => void; + trackKeySelectedForEdit?: string | null; + setTrackKeySelectedForEdit?: (key: string | null) => void; + clinicalTrackColorChanged?: boolean; + setClinicalTrackColorChanged?: (changed: boolean) => void; + suppressRendering?: boolean; onSuppressRendering?: () => void; onReleaseRendering?: () => void; diff --git a/src/shared/components/oncoprint/OncoprintUtils.ts b/src/shared/components/oncoprint/OncoprintUtils.ts index acc75f0ca17..2a294750e1c 100644 --- a/src/shared/components/oncoprint/OncoprintUtils.ts +++ b/src/shared/components/oncoprint/OncoprintUtils.ts @@ -32,8 +32,10 @@ import { makeGeneticTrackData, makeHeatmapTrackData, } from './DataUtils'; -import ResultsViewOncoprint from './ResultsViewOncoprint'; import _, { isNumber } from 'lodash'; +import ResultsViewOncoprint, { + getClinicalTrackValues, +} from './ResultsViewOncoprint'; import { action, IObservableArray, ObservableMap, runInAction } from 'mobx'; import { MobxPromise } from 'mobxpromise'; import GenesetCorrelatedGeneCache from 'shared/cache/GenesetCorrelatedGeneCache'; @@ -54,7 +56,11 @@ import { MUTATION_SPECTRUM_FILLS, SpecialAttribute, } from '../../cache/ClinicalDataCache'; -import { hexToRGBA, RESERVED_CLINICAL_VALUE_COLORS } from 'shared/lib/Colors'; +import { + ASCN_WHITE, + hexToRGBA, + RESERVED_CLINICAL_VALUE_COLORS, +} from 'shared/lib/Colors'; import { ISelectOption } from './controls/OncoprintControls'; import ifNotDefined from '../../lib/ifNotDefined'; import { @@ -446,7 +452,8 @@ export function getGenesetHeatmapTrackRuleSetParams() { export function getGeneticTrackRuleSetParams( distinguishMutationType?: boolean, distinguishDrivers?: boolean, - distinguishGermlineMutations?: boolean + distinguishGermlineMutations?: boolean, + isWhiteBackgroundForGlyphsEnabled?: boolean ): IGeneticAlterationRuleSetParams { let rule_set; if (!distinguishMutationType && !distinguishDrivers) { @@ -462,6 +469,18 @@ export function getGeneticTrackRuleSetParams( if (distinguishGermlineMutations) { Object.assign(rule_set.rule_params.conditional, germline_rule_params); } + if (isWhiteBackgroundForGlyphsEnabled) { + rule_set.legend_base_color = hexToRGBA(ASCN_WHITE); + if (rule_set.rule_params.always) { + rule_set.rule_params.always.shapes = [ + { + type: 'rectangle', + fill: hexToRGBA(ASCN_WHITE), + z: 1, + }, + ]; + } + } return rule_set; } @@ -491,8 +510,8 @@ export function getClinicalTrackRuleSetParams(track: ClinicalTrackSpec) { category_key: 'attr_val', category_to_color: Object.assign( {}, - track.category_to_color, - _.mapValues(RESERVED_CLINICAL_VALUE_COLORS, hexToRGBA) + _.mapValues(RESERVED_CLINICAL_VALUE_COLORS, hexToRGBA), + track.category_to_color ), universal_rule_categories: track.universal_rule_categories, }; @@ -1031,6 +1050,11 @@ export function makeClinicalTracksMobxPromise( Yes: true, }; } + const userSelectedClinicalTracksColors = + oncoprint.props.store + .userSelectedStudiesToClinicalTracksColors['global'][ + attribute.displayName + ]; if (attribute.datatype === 'NUMBER') { ret.datatype = 'number'; if ( @@ -1067,9 +1091,10 @@ export function makeClinicalTracksMobxPromise( } } else if (attribute.datatype === 'STRING') { ret.datatype = 'string'; - (ret as any).category_to_color = _.mapValues( - dataAndColors.categoryToColor, - hexToRGBA + (ret as any).category_to_color = Object.assign( + {}, + _.mapValues(dataAndColors.categoryToColor, hexToRGBA), + userSelectedClinicalTracksColors ); } else if ( attribute.clinicalAttributeId === @@ -1077,7 +1102,16 @@ export function makeClinicalTracksMobxPromise( ) { ret.datatype = 'counts'; (ret as any).countsCategoryLabels = MUTATION_SPECTRUM_CATEGORIES; - (ret as any).countsCategoryFills = MUTATION_SPECTRUM_FILLS; + (ret as any).countsCategoryFills = MUTATION_SPECTRUM_FILLS.slice(); + _.forEach((ret as any).countsCategoryLabels, (label, i) => { + if ( + userSelectedClinicalTracksColors && + userSelectedClinicalTracksColors[label] + ) { + (ret as any).countsCategoryFills[i] = + userSelectedClinicalTracksColors[label]; + } + }); } const trackConfig = diff --git a/src/shared/components/oncoprint/ResultsViewOncoprint.tsx b/src/shared/components/oncoprint/ResultsViewOncoprint.tsx index b3ccc60fac7..2acdc274812 100644 --- a/src/shared/components/oncoprint/ResultsViewOncoprint.tsx +++ b/src/shared/components/oncoprint/ResultsViewOncoprint.tsx @@ -55,12 +55,14 @@ 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 { OncoprintJS, RGBAColor, TrackGroupIndex, TrackId } from 'oncoprintjs'; import fileDownload from 'react-file-download'; import tabularDownload from './tabularDownload'; import classNames from 'classnames'; import { clinicalAttributeIsLocallyComputed, + MUTATION_SPECTRUM_CATEGORIES, + MUTATION_SPECTRUM_FILLS, SpecialAttribute, } from '../../cache/ClinicalDataCache'; import OqlStatusBanner from '../banners/OqlStatusBanner'; @@ -90,6 +92,10 @@ import '../../../globalStyles/oncoprintStyles.scss'; import { GenericAssayTrackInfo } from 'pages/studyView/addChartButton/genericAssaySelection/GenericAssaySelection'; import { toDirectionString } from './SortUtils'; import { RestoreClinicalTracksMenu } from 'pages/resultsView/oncoprint/RestoreClinicalTracksMenu'; +import { Modal } from 'react-bootstrap'; +import ClinicalTrackColorPicker from './ClinicalTrackColorPicker'; +import { hexToRGBA, rgbaToHex } from 'shared/lib/Colors'; +import classnames from 'classnames'; interface IResultsViewOncoprintProps { divId: string; @@ -132,6 +138,46 @@ export type AdditionalTrackGroupRecord = { molecularProfile: MolecularProfile; }; +export function getClinicalTrackValues(track: ClinicalTrackSpec): any[] { + // if the datatype is "counts", the values are under countsCategoryLabels + // else if the datatype is "string", get values from the track data + if (track.datatype === 'counts') { + return track.countsCategoryLabels; + } else if (track.datatype === 'string') { + const values = _(track.data) + .map(d => d.attr_val) + .uniq() + .without(undefined) + .value(); + return values.sort((a: string, b: string) => + a < b ? -1 : a > b ? 1 : 0 + ); + } + return []; +} + +export function getClinicalTrackColor( + track: ClinicalTrackSpec, + value: string +): RGBAColor { + if (track.datatype === 'counts') { + // get index of value that corresponds with its color + let valueIndex = _.indexOf(track.countsCategoryLabels, value); + return track.countsCategoryFills[valueIndex]; + } else if (track.datatype === 'string' && track.category_to_color) { + if ( + (track.label === 'Sample Type' || + track.label === 'Sample type id') && + value === 'Mixed' + ) { + return track.category_to_color[value] || [48, 97, 194, 1]; + } + return track.category_to_color[value]; + } else { + return [255, 255, 255, 1] as RGBAColor; + } +} + /* fields and methods in the class below are ordered based on roughly /* chronological setup concerns, rather than on encapsulation and public API */ /* tslint:disable: member-ordering */ @@ -184,6 +230,12 @@ export default class ResultsViewOncoprint extends React.Component< ); } + @computed get isWhiteBackgroundForGlyphsEnabled() { + return ( + this.urlWrapper.query.enable_white_background_for_glyphs === 'true' + ); + } + @computed get genericAssayPromises() { if (this.props.store.studyIds.result.length === 1) { // we only support generic assay in oncoprint for single study, @@ -474,6 +526,9 @@ export default class ResultsViewOncoprint extends React.Component< get showOqlInLabels() { return self.showOqlInLabels; }, + get isWhiteBackgroundForGlyphsEnabled() { + return self.isWhiteBackgroundForGlyphsEnabled; + }, get showMinimap() { return self.showMinimap; }, @@ -700,6 +755,11 @@ export default class ResultsViewOncoprint extends React.Component< onSelectShowOqlInLabels: (show: boolean) => { this.showOqlInLabels = show; }, + onSelectIsWhiteBackgroundForGlyphsEnabled: (s: boolean) => { + this.urlWrapper.updateURL({ + enable_white_background_for_glyphs: s.toString(), + }); + }, onSelectShowMinimap: (show: boolean) => { this.showMinimap = show; }, @@ -1794,6 +1854,76 @@ export default class ResultsViewOncoprint extends React.Component< return WindowStore.size.width - 75; } + @action.bound + public handleSelectedClinicalTrackColorChange( + value: string, + color: RGBAColor | undefined + ) { + if (this.selectedClinicalTrack) { + this.props.store.setUserSelectedClinicalTrackColor( + this.selectedClinicalTrack.label, + value, + color + ); + } + } + + @observable trackKeySelectedForEdit: string | null = null; + + @action.bound + setTrackKeySelectedForEdit(key: string | null) { + this.trackKeySelectedForEdit = key; + } + + @computed get selectedClinicalTrack() { + return _.find( + this.clinicalTracks.result, + t => t.key === this.trackKeySelectedForEdit + ); + } + + @autobind + private getDefaultSelectedClinicalTrackColor(value: string) { + if (!this.selectedClinicalTrack) { + return [255, 255, 255, 1]; + } + if (this.selectedClinicalTrack.datatype === 'counts') { + return MUTATION_SPECTRUM_FILLS[ + _.indexOf(MUTATION_SPECTRUM_CATEGORIES, value) + ]; + } else if (this.selectedClinicalTrack.datatype === 'string') { + if ( + (this.selectedClinicalTrack.label === 'Sample Type' || + this.selectedClinicalTrack.label === 'Sample type id') && + value === 'Mixed' + ) { + return [48, 97, 194, 1]; + } else { + return hexToRGBA( + this.props.store.clinicalDataCache.get( + this.props.store.clinicalAttributeIdToClinicalAttribute + .result![this.selectedClinicalTrack!.attributeId] + ).result!.categoryToColor![value] + ); + } + } + return [255, 255, 255, 1]; + } + + @computed get selectedClinicalTrackValues() { + if (this.selectedClinicalTrack) { + return getClinicalTrackValues(this.selectedClinicalTrack); + } + return []; + } + + @observable clinicalTrackColorChanged: boolean = false; + + @action.bound + setClinicalTrackColorChanged(changed: boolean) { + this.clinicalTrackColorChanged = changed; + } + public render() { getBrowserWindow().donk = this; return ( @@ -1831,6 +1961,100 @@ export default class ResultsViewOncoprint extends React.Component< /> + {this.selectedClinicalTrack && ( + this.setTrackKeySelectedForEdit(null)} + > + + + Color Configuration:{' '} + {this.selectedClinicalTrack.label} + + + + + + + + + + + + {this.selectedClinicalTrackValues.map( + value => ( + + + + + ) + )} + +
ValueColor
{value} + +
+ +
+
+ )} +
diff --git a/src/shared/components/oncoprint/controls/OncoprintControls.tsx b/src/shared/components/oncoprint/controls/OncoprintControls.tsx index f08855e191a..c77c4f38b32 100644 --- a/src/shared/components/oncoprint/controls/OncoprintControls.tsx +++ b/src/shared/components/oncoprint/controls/OncoprintControls.tsx @@ -58,6 +58,7 @@ export interface IOncoprintControlsHandlers onSelectShowMinimap: (showMinimap: boolean) => void; onSelectDistinguishMutationType: (distinguish: boolean) => void; onSelectDistinguishGermlineMutations: (distinguish: boolean) => void; + onSelectIsWhiteBackgroundForGlyphsEnabled?: (use: boolean) => void; onSelectHideVUS: (hide: boolean) => void; onSelectHideGermlineMutations: (hide: boolean) => void; @@ -90,6 +91,7 @@ export interface IOncoprintControlsState showClinicalTrackLegends?: boolean; onlyShowClinicalLegendForAlteredCases?: boolean; showOqlInLabels?: boolean; + isWhiteBackgroundForGlyphsEnabled?: boolean; showMinimap: boolean; isClinicalTrackConfigDirty: boolean; isLoggedIn: boolean; @@ -151,6 +153,7 @@ const EVENT_KEY = { showClinicalTrackLegends: '4', onlyShowClinicalLegendForAlteredCases: '4.1', showOqlInLabels: '4.2', + isWhiteBackgroundForGlyphsEnabled: '4.3', distinguishMutationType: '5', distinguishGermlineMutations: '5.1', sortByMutationType: '6', @@ -276,6 +279,12 @@ export default class OncoprintControls extends React.Component< !this.props.state.showOqlInLabels ); break; + case EVENT_KEY.isWhiteBackgroundForGlyphsEnabled: + this.props.handlers.onSelectIsWhiteBackgroundForGlyphsEnabled && + this.props.handlers.onSelectIsWhiteBackgroundForGlyphsEnabled( + !this.props.state.isWhiteBackgroundForGlyphsEnabled + ); + break; case EVENT_KEY.columnTypeSample: this.props.handlers.onSelectColumnType && this.props.handlers.onSelectColumnType('sample'); @@ -1046,6 +1055,23 @@ export default class OncoprintControls extends React.Component< Show OQL filters +
+ +
); }); diff --git a/src/shared/lib/Colors.ts b/src/shared/lib/Colors.ts index 9546100ae7d..4913e3dce63 100644 --- a/src/shared/lib/Colors.ts +++ b/src/shared/lib/Colors.ts @@ -13,6 +13,7 @@ import { STRUCTURAL_VARIANT_COLOR, } from 'cbioportal-frontend-commons'; import { MUT_PROFILE_COUNT_NOT_MUTATED } from 'pages/resultsView/plots/PlotsTabUtils'; +import { RGBAColor } from 'oncoprintjs'; // Default grey export const BLACK = '#000000'; export const LIGHT_GREY = '#D3D3D3'; @@ -178,3 +179,19 @@ export function hexToRGBA(str: string): [number, number, number, number] { const b = parseInt(str[5] + str[6], 16); return [r, g, b, 1]; } + +export function rgbaToHex(rgba: RGBAColor): string { + let hexR = rgba[0].toString(16); + let hexG = rgba[1].toString(16); + let hexB = rgba[2].toString(16); + if (hexR.length === 1) { + hexR = '0' + hexR; + } + if (hexG.length === 1) { + hexG = '0' + hexG; + } + if (hexB.length === 1) { + hexB = '0' + hexB; + } + return `#${hexR}${hexG}${hexB}`; +}