Skip to content

Commit

Permalink
Add the proteinInfoPanel for multiple species.
Browse files Browse the repository at this point in the history
  • Loading branch information
yjcyxky committed Mar 24, 2024
1 parent 28942b0 commit 7c52b9f
Show file tree
Hide file tree
Showing 13 changed files with 491 additions and 85 deletions.
1 change: 1 addition & 0 deletions studio/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"rc-menu": "^9.1.0",
"rc-util": "^5.16.0",
"react": "^18.0.0",
"react-alignment-viewer": "^0.5.5",
"react-ansi": "^3.0.2",
"react-chart-editor": "^0.45.0",
"react-cookie-consent": "^8.0.1",
Expand Down
23 changes: 23 additions & 0 deletions studio/src/NodeInfoPanel/AlignmentViewer/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React from 'react';
import { Row } from 'antd';
// @ts-ignore
import { AlignmentViewer as ReactAlignmentViewer } from 'react-alignment-viewer';
import type { AlignmentData } from '../index.t';

type AlignmentViewerProps = {
data: AlignmentData[];
};

function transformDataForAlignmentViewer(dataArray: AlignmentData[]): string[] {
return dataArray.map(item => `>${item.species}|${item.geneSymbol}|${item.entrezgene}\n${item.sequence}`);
}

const AlignmentViewer: React.FC<AlignmentViewerProps> = (props) => {
const [dataset, setDataset] = React.useState<string[]>(transformDataForAlignmentViewer(props.data));

return <Row className="alignment-viewer-container" style={{ width: '100%', height: '100%' }}>
<ReactAlignmentViewer data={dataset} />
</Row>
}

export default AlignmentViewer;
7 changes: 7 additions & 0 deletions studio/src/NodeInfoPanel/ComposedProteinPanel/index.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.composed-protein-panel {
.ant-tabs-tab {
text-align: left;
width: 80px;
padding: 8px 0 !important;
}
}
169 changes: 169 additions & 0 deletions studio/src/NodeInfoPanel/ComposedProteinPanel/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import { Empty, Tabs } from "antd";
import React, { useEffect, useState } from "react";
import { fetchMyGeneInfo, isProteinCoding, fetchProteinInfo } from "../ProteinInfoPanel/utils";
import type { GeneInfo, UniProtEntry } from "../index.t";
import ProteinInfoPanel from "../ProteinInfoPanel";
import AlignmentViewer from "../AlignmentViewer";
import GeneInfoPanel from "../GeneInfoPanel";
import { guessSpecies, isExpectedSpecies, expectedOrder, guessSpeciesAbbr } from '@/components/util';

import "./index.less";

type ComposedProteinPanel = {
geneInfo: GeneInfo;
}

const fetchProteinInfoByGeneInfo = async (geneInfo: GeneInfo): Promise<UniProtEntry> => {
const uniprotId = geneInfo.uniprot ? geneInfo.uniprot['Swiss-Prot'] : null;
if (!uniprotId) {
return {} as UniProtEntry;
}

return fetchProteinInfo(uniprotId);
}

const ComposedProteinPanel: React.FC<ComposedProteinPanel> = (props) => {
const { geneInfo } = props;
const [items, setItems] = useState<any[]>([]);
const [allGeneInfos, setAllGeneInfos] = useState<{
taxid: number;
geneInfo: GeneInfo;
species: string;
abbr: string;
}[]>([]);
const [allProteinInfos, setAllProteinInfos] = useState<Record<string, {
proteinInfo: UniProtEntry;
geneInfo: GeneInfo;
}>>({});

useEffect(() => {
const init = async () => {
if (!geneInfo) {
return;
}

const proteinInfo = await fetchProteinInfoByGeneInfo(geneInfo);
setItems([
{
label: guessSpeciesAbbr(`${geneInfo.taxid}`),
key: geneInfo.taxid,
children: isProteinCoding(geneInfo) ?
< ProteinInfoPanel geneInfo={geneInfo} proteinInfo={proteinInfo} /> :
<GeneInfoPanel geneSymbol={geneInfo.symbol} />
}
])

if (!geneInfo.homologene) {
return;
} else {
const remaingGenes = geneInfo.homologene.genes.filter(([taxid, entrezgene]) => {
return isExpectedSpecies(`${taxid}`)
})

const geneInfos = remaingGenes.map(([taxid, entrezgene]) => {
if (taxid === geneInfo.taxid) {
return geneInfo;
}
return fetchMyGeneInfo(entrezgene.toString());
});

Promise.all(geneInfos).then((geneInfos) => {
const oGeneInfos = geneInfos.map((geneInfo, index) => {
return {
taxid: geneInfo.taxid,
geneInfo,
species: guessSpecies(`${geneInfo.taxid}`),
abbr: guessSpeciesAbbr(`${geneInfo.taxid}`)
};
})
const orderedGeneInfos = oGeneInfos.sort((a, b) => {
return expectedOrder.indexOf(a.taxid.toString()) - expectedOrder.indexOf(b.taxid.toString());
});
setAllGeneInfos(orderedGeneInfos);
}).catch((error) => {
console.error(error);
setAllGeneInfos([]);
});

const proteinInfos = remaingGenes.map(([taxid, entrezgene]) => {
if (taxid === geneInfo.taxid) {
return proteinInfo;
}
return fetchProteinInfoByGeneInfo(geneInfo);
});

Promise.all(proteinInfos).then((proteinInfos) => {
const oProteinInfos = proteinInfos.map((proteinInfo, index) => {
return proteinInfo;
});

const proteinInfoMap: Record<string, {
proteinInfo: UniProtEntry;
geneInfo: GeneInfo;
}> = {};
oProteinInfos.forEach((proteinInfo, index) => {
const genePair = remaingGenes[index];
const taxid = genePair[0].toString();
const geneInfo = allGeneInfos.find((geneInfo) => geneInfo.taxid.toString() === taxid);
proteinInfoMap[taxid] = {
proteinInfo,
geneInfo: geneInfo?.geneInfo || {} as GeneInfo
}
});

setAllProteinInfos(proteinInfoMap);
}).catch((error) => {
console.error(error);
});
}
}

init();
}, []);

useEffect(() => {
let oItems = allGeneInfos.map((geneInfoMap) => {
const proteinInfo = allProteinInfos[geneInfoMap.taxid.toString()]?.proteinInfo;
return {
label: geneInfoMap.abbr,
key: geneInfoMap.taxid,
children: isProteinCoding(geneInfoMap.geneInfo) ?
< ProteinInfoPanel geneInfo={geneInfoMap.geneInfo} proteinInfo={proteinInfo} /> :
<GeneInfoPanel geneSymbol={geneInfoMap.geneInfo.symbol} />
}
});

if (oItems.length === 0) {
return;
} else {
const alignmentData = Object.keys(allProteinInfos).map((taxid) => {
const proteinInfo = allProteinInfos[taxid].proteinInfo;
const geneInfo = allProteinInfos[taxid].geneInfo;
return {
sequence: proteinInfo.sequence.value,
species: guessSpecies(taxid),
geneSymbol: geneInfo.symbol,
entrezgene: geneInfo.entrezgene
}
});
oItems.push({
label: 'Alignment',
key: oItems.length + 1,
children: <AlignmentViewer data={alignmentData} />
})
}

setItems(oItems);
}, [allGeneInfos]);

return (items.length === 0 ?
<Empty description="No information available." /> :
<Tabs
className="composed-protein-panel"
tabPosition="left"
items={items}
/>
)
}

export default ComposedProteinPanel;
39 changes: 13 additions & 26 deletions studio/src/NodeInfoPanel/ProteinInfoPanel/index.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { Empty, Row, Col, Badge, Descriptions, Table, Spin } from "antd";
import React, { useEffect, useState } from "react";
import type { GeneInfo, UniProtEntry } from "./index.t";
import type { GeneInfo, UniProtEntry } from "../index.t";
import { isProteinCoding, fetchProteinInfo } from "./utils";
import type { DescriptionsProps } from 'antd';
import { MolStarViewer } from "..";
import { guessSpecies } from "@/components/util";

import './index.less';

export interface ProteinInfoPanelProps {
geneInfo?: GeneInfo;
geneInfo: GeneInfo;
proteinInfo: UniProtEntry;
}

function PubMedLinks(text: string) {
Expand Down Expand Up @@ -153,30 +155,13 @@ export const PdbInfo: React.FC<{ proteinInfo: UniProtEntry }> = ({ proteinInfo }
}

export const ProteinInfoPanel: React.FC<ProteinInfoPanelProps> = (props) => {
const { geneInfo } = props;
const [proteinInfo, setProteinInfo] = useState<UniProtEntry | null>(null);
const { geneInfo, proteinInfo } = props;
// @ts-ignore
const [generalInfo, setGeneralInfo] = useState<DescriptionsProps['items']>([]);
const [loading, setLoading] = useState<boolean>(false);

useEffect(() => {
if (geneInfo && isProteinCoding(geneInfo)) {
setLoading(true);
const uniprotId = geneInfo.uniprot ? geneInfo.uniprot['Swiss-Prot'] : null;
if (!uniprotId) {
setProteinInfo(null);
return;
}

fetchProteinInfo(uniprotId).then((resp: UniProtEntry) => {
setProteinInfo(resp);
setLoading(false);
}).catch((err: any) => {
console.error(err);
setProteinInfo(null);
setLoading(false);
});

// @ts-ignore
const generalInfo: DescriptionsProps['items'] = [
{
Expand All @@ -199,7 +184,7 @@ export const ProteinInfoPanel: React.FC<ProteinInfoPanelProps> = (props) => {
{
key: 'alias',
label: 'Alias',
children: geneInfo.alias ? geneInfo.alias.join(', ') : null,
children: geneInfo.alias ? (geneInfo.alias.join ? geneInfo.alias.join(', ') : geneInfo.alias) : null,
},
{
key: 'location',
Expand All @@ -212,6 +197,11 @@ export const ProteinInfoPanel: React.FC<ProteinInfoPanelProps> = (props) => {
children: uniprotId ? <a href={`https://www.uniprot.org/uniprot/${uniprotId}`} target="_blank">
{uniprotId}
</a> : 'Unknown',
},
{
key: 'species',
label: 'Species',
children: guessSpecies(`${geneInfo.taxid}`)
}
];

Expand All @@ -220,7 +210,7 @@ export const ProteinInfoPanel: React.FC<ProteinInfoPanelProps> = (props) => {
}, [geneInfo]);

return (
proteinInfo ? (
Object.keys(proteinInfo).length === 0 ? <Empty description="No protein info found" /> :
<Row className="protein-info-panel">
<Col className="general-information">
{/* @ts-ignore */}
Expand All @@ -230,7 +220,7 @@ export const ProteinInfoPanel: React.FC<ProteinInfoPanelProps> = (props) => {
{proteinInfo ? (
getBiologyBackground(proteinInfo)
) : (
<Empty description="No protein found" />
<Empty description="No protein info found" />
)}
</Col>
<Col className="protein-snp">
Expand All @@ -242,9 +232,6 @@ export const ProteinInfoPanel: React.FC<ProteinInfoPanelProps> = (props) => {
<PdbInfo proteinInfo={proteinInfo} />
</Col>
</Row>
) : (
<Spin spinning={loading}><Empty description="No gene found" /></Spin>
)
);
}

Expand Down
2 changes: 1 addition & 1 deletion studio/src/NodeInfoPanel/ProteinInfoPanel/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { GeneInfo, UniProtEntry } from './index.t';
import { GeneInfo, UniProtEntry } from '../index.t';

export const fetchMyGeneInfo = async (entrezId: string): Promise<GeneInfo> => {
const response = await fetch(`https://mygene.info/v3/gene/${entrezId}?fields=all&dotfield=false&size=10`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ export interface GeneInfo {
summary?: string;
symbol: string;
taxid: number;
homologene?: {
genes: number[][], // Such as "genes": [[3702, 824036], [9606, 1017]], the first number is the taxid and the second number is the entrezid.
id: number;
};
type_of_gene: string;
unigene?: string[];
uniprot?: {
Expand Down Expand Up @@ -224,3 +228,10 @@ export interface CrossReferenceProperty {
key: string;
value: string;
}

export interface AlignmentData {
sequence: string;
species: string;
geneSymbol: string;
entrezgene: string;
};
8 changes: 4 additions & 4 deletions studio/src/NodeInfoPanel/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ import MolStarViewer from "./MolStarViewer";
import MutationViewer from "./MutationViewer";
import SangerCosmic from "./SangerCosmic";
import SgrnaSelector from "./SgrnaSelector";
import type { GeneInfo } from "./ProteinInfoPanel/index.t";
import { isProteinCoding, fetchMyGeneInfo } from "./ProteinInfoPanel/utils";
import ProteinInfoPanel from "./ProteinInfoPanel";
import type { GeneInfo } from "./index.t";
import { fetchMyGeneInfo } from "./ProteinInfoPanel/utils";
import React, { useEffect, useState } from "react";
import type { GraphNode } from "biominer-components/dist/typings";
import ComposedProteinPanel from "./ComposedProteinPanel";

import "./index.less";

Expand Down Expand Up @@ -59,7 +59,7 @@ const NodeInfoPanel: React.FC<{ node?: GraphNode, hiddenItems?: string[] }> = ({
{
label: "Summary",
key: "summary",
children: isProteinCoding(geneInfo) ? <ProteinInfoPanel geneInfo={geneInfo} /> : <GeneInfoPanel geneSymbol={geneSymbol} />
children: <ComposedProteinPanel geneInfo={geneInfo} />
},
{
label: "Gene Expression",
Expand Down
Loading

0 comments on commit 7c52b9f

Please sign in to comment.