import * as React from 'react';
import { observer } from 'mobx-react';
import { observable, action, computed, makeObservable } from 'mobx';
import { DownloadControls, DefaultTooltip } from 'cbioportal-frontend-commons';
import autobind from 'autobind-decorator';
import MultipleCategoryBarPlot from 'pages/groupComparison/MultipleCategoryBarPlot';
import ReactSelect from 'react-select';
import OQLTextArea, {
GeneBoxType,
} from 'shared/components/GeneSelectionBox/OQLTextArea';
import _ from 'lodash';
import { SingleGeneQuery } from 'shared/lib/oql/oql-parser';
import { Gene } from 'cbioportal-ts-api-client';
import {
getEnrichmentBarPlotData,
getGeneListOptions,
CNA_TO_ALTERATION,
GeneOptionLabel,
} from './EnrichmentsUtil';
import styles from './frequencyPlotStyles.module.scss';
import { AlterationEnrichmentRow } from 'shared/model/AlterationEnrichmentRow';
import { toConditionalPrecision } from 'shared/lib/NumberUtils';
import { FormControl } from 'react-bootstrap';
import { GeneReplacement } from 'shared/components/query/QueryStore';
import { EnrichmentsTableDataStore } from './EnrichmentsTableDataStore';
export interface IGeneBarPlotProps {
data: AlterationEnrichmentRow[];
groupOrder?: string[];
isTwoGroupAnalysis?: boolean;
showCNAInTable?: boolean;
yAxisLabel: string;
categoryToColor?: {
[id: string]: string;
};
dataStore: EnrichmentsTableDataStore;
}
const DEFAULT_GENES_COUNT = 10;
const MAXIMUM_ALLOWED_GENES = 100;
const CHART_BAR_WIDTH = 10;
const ALLOWED_CNA_TYPES = ['AMP', 'HOMDEL'];
@observer
export default class GeneBarPlot extends React.Component<
IGeneBarPlotProps,
{}
> {
@observable tooltipModel: any;
@observable.ref _geneQuery: string | undefined = undefined;
@observable selectedGenes: SingleGeneQuery[] | undefined;
@observable _label: GeneOptionLabel | undefined;
@observable isGeneSelectionPopupVisible: boolean | undefined = false;
@observable private svgContainer: SVGElement | null;
constructor(props: any) {
super(props);
makeObservable(this);
}
@computed get geneListOptions() {
return getGeneListOptions(this.props.data, this.props.showCNAInTable);
}
@computed get defaultOption() {
return this.geneListOptions.length > 1
? this.geneListOptions[1]
: this.geneListOptions[0];
}
@computed get geneDataSet() {
return _.keyBy(this.props.data, datum => {
if (this.props.showCNAInTable) {
//add copy number alteration type 'amp' or 'del'
return (
datum.hugoGeneSymbol +
`: ${CNA_TO_ALTERATION[datum.value!]}`
);
} else {
return datum.hugoGeneSymbol;
}
});
}
@computed get barPlotData() {
return getEnrichmentBarPlotData(
this.geneDataSet,
this.barPlotOrderedGenes
);
}
@computed get barPlotOrderedGenes() {
let genes: string[] = [];
if (this._label === GeneOptionLabel.SYNC_WITH_TABLE) {
return this.tableSelectedGenes;
}
if (!this.selectedGenes) {
genes = this.defaultOption.genes.slice(0, DEFAULT_GENES_COUNT);
} else {
// Add alteration to genes in case when both AMP and DEL both show togehter in table
if (this.props.showCNAInTable) {
genes = _.flatMap(this.selectedGenes, geneWithAlteration => {
//if no alteration sepcified include both AMP and HOMDEL
if (geneWithAlteration.alterations === false) {
return [
geneWithAlteration.gene + ': AMP',
geneWithAlteration.gene + ': HOMDEL',
];
}
const geneKeys: string[] = [];
_.each(geneWithAlteration.alterations, alteration => {
if (
alteration.alteration_type === 'cna' &&
alteration.constr_rel === '=' &&
ALLOWED_CNA_TYPES.includes(alteration.constr_val!)
) {
geneKeys.push(
geneWithAlteration.gene +
': ' +
alteration.constr_val
);
}
});
return geneKeys;
});
} else {
genes = this.selectedGenes.map(
geneWithAlteration => geneWithAlteration.gene
);
}
}
return genes;
}
@computed get horzCategoryOrder() {
//include significant genes
return _.flatMap(this.barPlotOrderedGenes, gene => [gene + '*', gene]);
}
@autobind
private getTooltip(datum: any) {
let geneSymbol = datum.majorCategory as string;
// get rid of a trailing *
geneSymbol = geneSymbol.replace(/\*$/, '');
let geneData = this.geneDataSet[geneSymbol];
//use groupOrder inorder of sorted groups
let groupRows = _.map(this.props.groupOrder, groupName => {
const group = geneData.groupsSet[groupName];
let style: any = {};
//bold row corresponding to highlighed bar
if (datum.minorCategory === group.name) {
style = { fontWeight: 'bold' };
}
return (
| {group.name} |
{group.alteredPercentage.toFixed(2)}% (
{group.alteredCount}/{group.profiledCount})
|
);
});
return (
{geneSymbol} {this.props.yAxisLabel}
| Group |
Percentage Altered |
{groupRows}
p-Value:{' '}
{geneData.pValue
? toConditionalPrecision(geneData.pValue, 3, 0.01)
: '-'}
q-Value:{' '}
{geneData.qValue
? toConditionalPrecision(geneData.qValue, 3, 0.01)
: '-'}
);
}
@computed private get selectedOption() {
if (this._label && this._geneQuery !== undefined) {
return {
label: this._label,
value: this._geneQuery,
};
}
//default option
return {
label: this.defaultOption.label,
value: this.defaultOption.genes
.slice(0, DEFAULT_GENES_COUNT)
.join('\n'),
};
}
@computed get toolbar() {
return (
{this.selectedOption.label}
{
this.isGeneSelectionPopupVisible = visible;
}}
overlay={
{
this._geneQuery = value;
this.selectedGenes = genes;
this._label = label;
this.isGeneSelectionPopupVisible = false;
}}
defaultNumberOfGenes={DEFAULT_GENES_COUNT}
/>
}
placement="bottomLeft"
>
this.svgContainer}
filename={'GroupComparisonGeneFrequencyPlot'}
dontFade={true}
type="button"
/>
);
}
@computed private get tableSelectedGenes() {
if (this.props.dataStore.visibleData !== null) {
return this.props.dataStore.visibleData
.map(x => x.hugoGeneSymbol)
.slice(0, MAXIMUM_ALLOWED_GENES);
}
return [];
}
public render() {
return (
{this.toolbar}
(this.svgContainer = ref)}
/>
);
}
}
interface IGeneSelectionProps {
options: { label: GeneOptionLabel; genes: string[] }[];
selectedOption?: { label: GeneOptionLabel; value: string };
onSelectedGenesChange: (
value: string,
orderedGenes: SingleGeneQuery[],
label: GeneOptionLabel
) => void;
defaultNumberOfGenes: number;
maxNumberOfGenes?: number;
}
@observer
export class GenesSelection extends React.Component {
static defaultProps: Partial = {
maxNumberOfGenes: MAXIMUM_ALLOWED_GENES,
};
constructor(props: IGeneSelectionProps) {
super(props);
makeObservable(this);
(window as any).genesSelection = this;
}
@observable.ref _geneQuery: string | undefined = undefined;
@observable selectedGenesHasError = false;
@observable private numberOfGenes = this.props.defaultNumberOfGenes;
@observable private _selectedGeneListOption:
| {
label: GeneOptionLabel;
value: string;
genes: string[];
}
| undefined;
@observable.ref genesToPlot: SingleGeneQuery[] = [];
@computed get geneListOptions() {
return _.map(this.props.options, option => {
return {
label: option.label,
value: option.genes.join('\n'),
};
});
}
@computed get geneOptionSet() {
return _.keyBy(this.props.options, option => option.label);
}
@computed get selectedGeneListOption() {
if (
this._selectedGeneListOption === undefined &&
this.props.selectedOption
) {
const selectedOption = this.props.selectedOption;
return this.geneListOptions.find(opt =>
opt.value.startsWith(selectedOption.value)
);
}
return this._selectedGeneListOption;
}
@computed get isCustomGeneSelection() {
return (
this.selectedGeneListOption === undefined ||
this.selectedGeneListOption.label ===
GeneOptionLabel.USER_DEFINED_OPTION
);
}
@computed get geneQuery() {
return this._geneQuery === undefined && this.props.selectedOption
? this.props.selectedOption.value
: this._geneQuery || '';
}
@action.bound
private onChangeGeneInput(
oql: { query: SingleGeneQuery[]; error?: any },
genes: { found: Gene[]; suggestions: GeneReplacement[] },
queryStr: string
) {
const foundGenes = _.keyBy(genes.found, gene =>
gene.hugoGeneSymbol.toUpperCase()
);
const queriedGenes = _.map(oql.query, query =>
query.gene.toUpperCase()
);
if (!_.isEmpty(foundGenes) || !_.isEmpty(genes.suggestions)) {
this.selectedGenesHasError = !_.every(
queriedGenes,
gene => gene in foundGenes
);
}
if (!this.selectedGenesHasError) {
this.genesToPlot = oql.query;
}
if (this.geneQuery !== queryStr) {
this._selectedGeneListOption = {
label: GeneOptionLabel.USER_DEFINED_OPTION,
genes: [],
value: '',
};
}
this._geneQuery = queryStr;
}
@computed get hasUnsupportedOQL() {
const geneWithUnsupportedOql = _.find(this.genesToPlot, gene => {
if (gene.alterations && gene.alterations.length > 0) {
let unsupportedAlteration = _.find(
gene.alterations,
alteration => {
// CNAs must have '=', and 'AMP or 'HOMDEL'
if (alteration.alteration_type === 'cna') {
return (
alteration.constr_rel !== '=' ||
!ALLOWED_CNA_TYPES.includes(
alteration.constr_val!
)
);
}
// For other alteration types anything is allowed
return false;
}
);
return unsupportedAlteration !== undefined;
}
return false;
});
return geneWithUnsupportedOql !== undefined;
}
@computed get addGenesButtonDisabled() {
if (this.inSyncMode) {
return (
this.props.selectedOption !== undefined &&
this.props.selectedOption.label ===
GeneOptionLabel.SYNC_WITH_TABLE
);
} else {
return (
this.hasUnsupportedOQL ||
(this.props.selectedOption &&
this.props.selectedOption.value === this._geneQuery) ||
this.selectedGenesHasError ||
_.isEmpty(this._geneQuery)
);
}
}
@action.bound
public onGeneListOptionChange(option: any) {
this._selectedGeneListOption = option;
if (option.value !== '') {
const genes = this.geneOptionSet[option.label].genes;
this._geneQuery = genes.slice(0, this.numberOfGenes).join('\n');
} else {
this._geneQuery = '';
}
}
@computed private get inSyncMode() {
return (
this._selectedGeneListOption &&
this._selectedGeneListOption.label ===
GeneOptionLabel.SYNC_WITH_TABLE
);
}
@action.bound
private handleTotalInputChange(e: any) {
const newCount: number = e.target.value.replace(/[^0-9]/g, '');
if (newCount <= this.props.maxNumberOfGenes!) {
this.numberOfGenes = newCount;
}
}
@action.bound
private handleTotalInputKeyPress(target: any) {
if (target.charCode === 13) {
if (isNaN(this.numberOfGenes)) {
this.numberOfGenes = 0;
return;
}
this.updateGeneQuery();
}
}
@action.bound
private onBlur() {
if (isNaN(this.numberOfGenes)) {
this.numberOfGenes = 0;
return;
}
this.updateGeneQuery();
}
@action.bound
private updateGeneQuery() {
//removes leading 0s
this.numberOfGenes = Number(this.numberOfGenes);
if (this.selectedGeneListOption) {
const label = this.selectedGeneListOption.label;
const genes = this.geneOptionSet[label].genes;
if (genes.length > 0) {
this._geneQuery = genes.slice(0, this.numberOfGenes).join('\n');
}
}
}
public render() {
return (
{this.props.options.length > 0 && (
)}
{!this.inSyncMode && !this.isCustomGeneSelection && (
)}
{!this.inSyncMode && (
{this.hasUnsupportedOQL && (
{`OQL is not allowed`}
)}
)}
);
}
}