import classnames from 'classnames';
import * as React from 'react';
import _ from 'lodash';
import { CopyNumberSeg, Mutation, Sample } from 'cbioportal-ts-api-client';
import { DefaultTooltip } from 'cbioportal-frontend-commons';
import SampleManager from '../SampleManager';
import GenePanelManager from '../GenePanelManager';
import { action, computed, makeObservable, observable } from 'mobx';
import { observer } from 'mobx-react';
import {
IKeyedIconData,
IGV_TRACK_SAMPLE_EXPAND_HEIGHT,
} from './GenomicOverviewUtils';
import {
calcIgvTrackHeight,
CNA_TRACK_NAME,
defaultGrch37ReferenceProps,
defaultGrch38ReferenceProps,
defaultMutationTrackProps,
defaultSegmentTrackProps,
generateMutationFeatures,
generateSegmentFeatures,
MUTATION_TRACK_NAME,
MutationTrackFeatures,
SegmentTrackFeatures,
WHOLE_GENOME,
} from 'shared/lib/IGVUtils';
import { isGrch38 } from 'shared/lib/referenceGenomeUtils';
import IntegrativeGenomicsViewer from 'shared/components/igv/IntegrativeGenomicsViewer';
import CnaTrackSampleSummary from './CnaTrackSampleSummary';
import CustomIgvColumn from './CustomIgvColumn';
import GenePanelIcon from './GenePanelIcon';
import MutationTrackSampleSummary from './MutationTrackSampleSummary';
import styles from './styles.module.scss';
interface IGenomicOverviewProps {
mergedMutations: Mutation[][];
cnaSegments: CopyNumberSeg[];
samples: Sample[];
sampleOrder: { [s: string]: number };
sampleLabels: { [s: string]: string };
sampleColors: { [s: string]: string };
sampleManager: SampleManager;
containerWidth: number;
sampleIdToMutationGenePanelId?: { [sampleId: string]: string };
sampleIdToCopyNumberGenePanelId?: { [sampleId: string]: string };
onSelectGenePanel?: (name: string) => void;
disableTooltip?: boolean;
store?: IGenomicOverviewStore;
onResetView: () => void;
genome?: string;
}
interface IGenomicOverviewStore {
activeLocus: string | undefined;
}
class DefaultGenomicOverviewStore implements IGenomicOverviewStore {
@observable
public activeLocus: string | undefined;
}
function getCnaTrackSampleSummariesForIgvTrack(
features: SegmentTrackFeatures[] = []
) {
return _.entries(_.groupBy(features, f => f.sample)).map(
([sample, featuresGroupedBySample]) => (
)
);
}
function getMutationTrackSampleSummariesForIgvTrack(
features: MutationTrackFeatures[] = []
) {
return _.entries(_.groupBy(features, f => f.sample)).map(
([sample, featuresGroupedBySample]) => (
)
);
}
function getGenePanelInfoForIgvTrack(
features: { sample: string }[] = [],
genePanelIconData: IKeyedIconData,
dataTextPrefix: string = 'genepanel-icon'
) {
return _.entries(_.groupBy(features, f => f.sample)).map(
([sample, featuresGroupedBySample], index) => (
)
);
}
function getSampleIds(sortedSamples: { sampleId: string }[]) {
return _.uniq(sortedSamples.map(s => s.sampleId));
}
// TODO see if it is possible to pass custom components for "customButtons"
const ResetZoomButton: React.FunctionComponent<{
handleResetZoom?: () => void;
className?: string;
}> = props => (
);
// TODO see if it is possible to pass custom components for "customButtons"
const ToggleButton: React.FunctionComponent<{
handleSwitchView?: () => void;
compactIgvView?: boolean;
className?: string;
}> = props => (
Advanced
);
@observer
export default class GenomicOverview extends React.Component<
IGenomicOverviewProps
> {
@observable compactIgvView = true;
private store: IGenomicOverviewStore;
private genePanelManager: GenePanelManager;
constructor(props: IGenomicOverviewProps) {
super(props);
makeObservable(this);
this.store = props.store || new DefaultGenomicOverviewStore();
this.genePanelManager = new GenePanelManager(
props.sampleIdToMutationGenePanelId,
props.sampleIdToCopyNumberGenePanelId
);
}
public get tracks() {
const tracks: any[] = [];
const compareFn = (a: { sampleId: string }, b: { sampleId: string }) =>
this.props.sampleOrder[a.sampleId] -
this.props.sampleOrder[b.sampleId];
if (this.props.mergedMutations.length > 0) {
const sortedMutations = _.flatten(this.props.mergedMutations).sort(
compareFn
);
const mutFeatures = generateMutationFeatures(sortedMutations);
const mutHeight = calcIgvTrackHeight(
mutFeatures,
600,
25,
IGV_TRACK_SAMPLE_EXPAND_HEIGHT
);
tracks.push({
...defaultMutationTrackProps(),
displayMode: 'EXPAND',
sampleExpandHeight: IGV_TRACK_SAMPLE_EXPAND_HEIGHT,
height: mutHeight,
features: mutFeatures,
samples: getSampleIds(sortedMutations),
});
}
if (this.props.cnaSegments.length > 0) {
// sort segments by sample order
const sortedSegments = this.props.cnaSegments
.slice()
.sort(compareFn);
const segFeatures = generateSegmentFeatures(sortedSegments);
const segHeight = calcIgvTrackHeight(
segFeatures,
600,
25,
IGV_TRACK_SAMPLE_EXPAND_HEIGHT
);
tracks.push({
...defaultSegmentTrackProps(),
displayMode: 'EXPAND',
sampleExpandHeight: IGV_TRACK_SAMPLE_EXPAND_HEIGHT,
height: segHeight,
features: segFeatures,
samples: getSampleIds(sortedSegments),
});
}
return tracks;
}
@computed get igvProps() {
const coreProps = {
locus: this.store.activeLocus,
onLocusChange: this.handleLocusChange,
tracks: this.tracks,
showTrackLabels: false,
showAllChromosomes: false,
showTrackControls: false,
disableContextMenuSort: true,
customButtons: [
// {
// label: this.compactIgvView ? 'Advanced' : 'Compact',
// callback: this.handleSwitchView,
// },
{
label: 'Reset View',
callback: this.handleResetView,
},
],
};
if (this.compactIgvView) {
return {
...coreProps,
showNavigation: false,
showSearch: false,
showSampleNameButton: false,
showChromosomeWidget: false,
showIdeogram: false,
showSVGButton: false,
showTrackLabelButton: false,
showCursorTrackingGuideButton: false,
showCenterGuideButton: false,
showSequenceTrack: false,
compactRulerTrack: true,
reference: {
...(isGrch38(this.props.genome || '')
? defaultGrch38ReferenceProps()
: defaultGrch37ReferenceProps()),
tracks: [], // do not include RefSeq track
},
};
} else {
return {
...coreProps,
genome: isGrch38(this.props.genome || '') ? 'hg38' : 'hg19',
showNavigation: true,
showSearch: true,
showSampleNameButton: true,
showChromosomeWidget: true,
showSVGButton: true,
showTrackLabelButton: true,
showCursorTrackingGuideButton: true,
showCenterGuideButton: true,
showIdeogram: true,
showSequenceTrack: true,
compactRulerTrack: false,
};
}
}
@computed get shouldShowGenePanelIcons() {
return (
!_.isEmpty(
this.genePanelManager.sampleIdToMutationGenePanelIconData
) ||
!_.isEmpty(
this.genePanelManager.sampleIdToCopyNumberGenePanelIconData
)
);
}
private getSampleIconsForIgvTrack(features: { sample: string }[] = []) {
return _.uniq(features.map(f => f.sample)).map(sampleId => (
{this.props.sampleManager.getComponentForSample(
sampleId,
1,
'',
null,
this.props.onSelectGenePanel
)}
));
}
public render() {
const tracks = this.tracks;
const mutationTrack = tracks.find(t => t.name === MUTATION_TRACK_NAME);
const cnaTrack = tracks.find(t => t.name === CNA_TRACK_NAME);
return (
{this.shouldShowGenePanelIcons && (
)}
);
}
private get tracksWidth(): number {
return this.props.containerWidth - 40;
}
@action.bound
private handleLocusChange(locus: string) {
// update the observable locus only if it is different than the current one
// no need to update (and re-render) if it is same as the current one
if (this.store.activeLocus !== locus) {
this.store.activeLocus = locus;
}
// switch to advanced view if locus is updated to anything other than whole genome
if (this.store.activeLocus !== WHOLE_GENOME) {
this.compactIgvView = false;
}
}
@action.bound
private handleResetZoom() {
// reset the observable locus to whole genome if it is not set to whole genome already
// no need to reset (and re-render) if it is already set to whole genome
if (this.store.activeLocus !== WHOLE_GENOME) {
this.handleLocusChange(WHOLE_GENOME);
}
}
@action.bound
private handleResetView() {
// reset to default view
this.compactIgvView = true;
this.handleResetZoom();
this.props.onResetView();
}
@action.bound
private handleSwitchView() {
this.compactIgvView = !this.compactIgvView;
}
}