import * as React from 'react';
import ProteinChainView from './ProteinChainView';
import PdbChainTable from './PdbChainTable';
import { observer } from 'mobx-react';
import {
computed,
observable,
action,
IReactionDisposer,
reaction,
makeObservable,
} from 'mobx';
import { ProteinChainSpec } from './ProteinChainView';
import { Collapse } from 'react-collapse';
import { HitZone, DefaultTooltip } from 'cbioportal-frontend-commons';
import MutationMapperStore from 'shared/components/mutationMapper/MutationMapperStore';
import { ALIGNMENT_GAP, IPdbChain } from '../../model/Pdb';
import PdbHeaderCache from '../../cache/PdbHeaderCache';
import PdbChainInfo from '../PdbChainInfo';
import onNextRenderFrame from 'shared/lib/onNextRenderFrame';
type ProteinChainPanelProps = {
store: MutationMapperStore;
geneWidth: number;
geneXOffset?: number;
maxChainsHeight: number;
pdbHeaderCache?: PdbHeaderCache;
};
@observer
export default class ProteinChainPanel extends React.Component<
ProteinChainPanelProps,
{}
> {
@observable private isExpanded: boolean = false;
@observable private pdbChainTableShown: boolean = false;
@observable private hoveredChain: IPdbChain | undefined;
@observable hitZoneConfig: any = {
x: 0,
y: 0,
width: 0,
height: 0,
onClick: () => {},
};
private chainDiv: HTMLDivElement;
private _chainScrollY: number = 0;
private collapseTimeout: number | null = null;
private expandTimeout: number | null = null;
private chainUidToY: { [uid: string]: number } = {};
private onChainSelectReaction: IReactionDisposer;
private expandDelayMs = 750;
private collapseDelayMs = 3000;
private handlers: any;
constructor(props: ProteinChainPanelProps) {
super(props);
makeObservable(this);
this.handlers = {
onMouseEnter: action(() => {
this.expandTimeout = window.setTimeout(() => {
this.isExpanded = true;
}, this.expandDelayMs);
if (this.collapseTimeout) {
window.clearTimeout(this.collapseTimeout);
}
}),
onMouseLeave: action(() => {
this.collapseTimeout = window.setTimeout(() => {
this.isExpanded = false;
}, this.collapseDelayMs);
if (this.expandTimeout) {
window.clearTimeout(this.expandTimeout);
}
}),
chainDivRef: (div: HTMLDivElement) => {
this.chainDiv = div;
},
onChainScroll: () => {
if (this.chainDiv && this.isExpanded) {
this._chainScrollY = this.chainDiv.scrollTop;
}
},
togglePDBTable: action(() => {
this.pdbChainTableShown = !this.pdbChainTableShown;
}),
getTooltipContent: () => {
if (this.hoveredChain) {
return (
);
} else {
return null;
}
},
setHitZone: (
hitRect: {
x: number;
y: number;
width: number;
height: number;
},
chainUid: string
) => {
this.hitZoneConfig.x = hitRect.x;
this.hitZoneConfig.y = hitRect.y;
this.hitZoneConfig.width = hitRect.width;
this.hitZoneConfig.height = hitRect.height;
this.hitZoneConfig.onClick = () => {
this.selectChain(chainUid);
};
this.hoveredChain = this.props.store.pdbChainDataStore.getPdbChain(
chainUid
);
},
setChainUidToY: (chainUidToY: { [uid: string]: number }) => {
this.chainUidToY = chainUidToY;
},
};
this.onChainSelectReaction = reaction(
() => this.props.store.pdbChainDataStore.selectedUid,
(selectedUid: string) => {
const chainY = this.chainUidToY[selectedUid];
if (
typeof chainY !== 'undefined' &&
(chainY < this.chainDiv.scrollTop ||
chainY >
this.chainDiv.scrollTop +
this.props.maxChainsHeight)
) {
const halfChainsHeight = this.props.maxChainsHeight / 2;
this.chainDiv.scrollTop = chainY - halfChainsHeight;
}
}
);
}
componentWillUnmount() {
this.onChainSelectReaction();
}
@computed get chainScrollY() {
if (this.isExpanded) {
return this._chainScrollY;
} else {
return 0;
}
}
@action private selectChain(chainUid: string) {
this.props.store.pdbChainDataStore.selectUid(chainUid);
}
@computed private get isOpen() {
return !!this.props.store.pdbChainDataStore.selectedChain;
}
@computed private get displayChains(): IPdbChain[] {
if (!this.props.store.pdbChainDataStore.selectedChain) {
return [];
} else if (!this.isExpanded) {
return [this.props.store.pdbChainDataStore.selectedChain];
} else {
return this.props.store.pdbChainDataStore.allData;
}
}
@computed get chains(): ProteinChainSpec[] {
return this.displayChains.map((pdbChain: IPdbChain) => {
const gaps = [];
let gapStart = -1;
const alignment = pdbChain.alignment;
for (let i = 0; i < alignment.length; i++) {
if (alignment[i] === ALIGNMENT_GAP) {
if (gapStart === -1) {
gapStart = i;
}
} else {
if (gapStart !== -1) {
gaps.push({
start: pdbChain.uniprotStart + gapStart,
end: pdbChain.uniprotStart + i,
});
gapStart = -1;
}
}
}
return {
start: pdbChain.uniprotStart,
end: pdbChain.uniprotEnd + 1,
gaps,
opacity: pdbChain.identityPerc,
uid: this.props.store.pdbChainDataStore.getChainUid(pdbChain),
};
});
}
@computed get proteinLength() {
const proteinLength =
(this.props.store.canonicalTranscript.result &&
this.props.store.canonicalTranscript.result.proteinLength) ||
0;
return Math.max(proteinLength, 1);
}
@computed get tooltipVisible() {
return this.handlers.getTooltipContent() !== null;
}
@computed get hitZone() {
return (
);
}
componentDidMount() {
onNextRenderFrame(() =>
this.props.store.pdbChainDataStore.selectFirstChain()
);
}
componentDidUpdate() {
onNextRenderFrame(() => {
if (this.chainDiv) {
this.chainDiv.scrollTop = this.chainScrollY;
}
});
}
public helpTooltipContent() {
return (
This panel displays a list of PDB chains for the corresponding
uniprot ID. PDB chains are ranked with respect to their sequence
similarity ratio, and aligned to the y-axis of the mutation
diagram. Highly ranked chains have darker color than the lowly
ranked ones.
Each chain is represented by a single rectangle. Gaps within the
chains are represented by a thin line connecting the segments of
the chain.
By default, only a first few rows are displayed. To see more
chains, use the scroll bar next to the panel. To see the
detailed list of all available PDB chains in a table click on
the link below the panel.
To select a chain, simply click on it. Selected chain is
highlighted with a different frame color. You can also select a
chain by clicking on a row in the table. Selecting a chain
reloads the PDB data for the 3D structure visualizer.
);
}
render() {
const tooltipVisibleProps: any = {};
if (!this.tooltipVisible) {
tooltipVisibleProps.visible = false;
}
return (
{/* Place holder for a possible PDB icon, for now manually aligning with a margin */}
PDB Chains
{this.pdbChainTableShown
? 'Hide PDB Chain Table'
: 'Show PDB Chain Table'}
);
}
}