Skip to content

Commit

Permalink
Add a msa library for aligning multiple sequences.
Browse files Browse the repository at this point in the history
  • Loading branch information
yjcyxky committed Mar 26, 2024
1 parent d08f4f0 commit 8c3c695
Show file tree
Hide file tree
Showing 6 changed files with 132 additions and 21 deletions.
1 change: 1 addition & 0 deletions studio/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"antd-schema-form": "^4.5.1",
"axios": "^1.1.2",
"biominer-components": "0.3.18",
"biomsa": "^0.3.3",
"classnames": "^2.3.0",
"handlebars": "^4.7.7",
"handsontable": "^12.1.3",
Expand Down
93 changes: 87 additions & 6 deletions studio/src/NodeInfoPanel/AlignmentViewer/index.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,119 @@
import React, { useEffect } from 'react';
import { Empty, Row } from 'antd';
import { Empty, Row, Spin } from 'antd';
// @ts-ignore
import { AlignmentViewer as ReactAlignmentViewer } from 'react-alignment-viewer';
import type { AlignmentData } from '../index.t';
import biomsa from 'biomsa';
import { expectedSpeciesOrder } from '@/components/util';

import './index.less';

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

function transformDataForAlignmentViewer(dataArray: AlignmentData[]): string[] {
const transformDataForAlignmentViewer = (dataArray: AlignmentData[]): string[] => {
return dataArray.map(item => {
const prefix = item.uniProtType === 'Swiss-Prot' ? 'sp' : 'tr';
return `>${prefix}|${item.uniProtId}|${item.proteinName} ${item.proteinDescription} OS=${item.species} GN=${item.geneSymbol} PE=${item.score} SV=${item.sequenceVersion}\n${item.sequence}`
});
}

const parseFasta = (fastaString: string) => {
// Split the string into lines
const lines = fastaString.trim().split(/\r?\n/);

// Initialize an array to hold the parsed records
const fastaRecords = [];

// Temporary variables to hold the current record's information
let currentId: string | null = null;
let currentSeq: string[] = [];

lines.forEach(line => {
if (line.startsWith('>')) { // Header line
if (currentId !== null) {
// Save the previous record
fastaRecords.push({
id: currentId,
seq: currentSeq.join('')
});
currentSeq = [];
}
// Extract ID (substring after '>' and before the first space if present)
currentId = line;
} else {
// Sequence line, append to current sequence
currentSeq.push(line);
}
});

// Don't forget to save the last record
if (currentId !== null) {
fastaRecords.push({
id: currentId,
seq: currentSeq.join('')
});
}

return fastaRecords;
}

const alignSeqs = async (seqs: string[]): Promise<string[]> => {
return biomsa.align(seqs, {
method: 'diag',
type: 'amino',
gapchar: '-',
})
}

const AlignmentViewer: React.FC<AlignmentViewerProps> = (props) => {
const [dataset, setDataset] = React.useState<string>("");
const [errorMsg, setErrorMsg] = React.useState<string>("No data");
const [loading, setLoading] = React.useState<boolean>(false);

useEffect(() => {
if (props.data && props.data.length > 0) {
const d = transformDataForAlignmentViewer(props.data);
setLoading(true);
const orderedData = props.data.sort((a, b) => {
const indexA = expectedSpeciesOrder.indexOf(a.species);
const indexB = expectedSpeciesOrder.indexOf(b.species);

// Handling unknown species by sorting them to the end
const unknownIndex = expectedSpeciesOrder.length;
return (indexA === -1 ? unknownIndex : indexA) - (indexB === -1 ? unknownIndex : indexB);
});
console.log("AlignmentViewer orderedData: ", props.data, orderedData);
const d = transformDataForAlignmentViewer(orderedData);
const dataset = d.join('\n');
console.log("AlignmentViewer dataset: ", dataset);
setDataset(dataset);

const fastaRecords = parseFasta(dataset);
const ids = fastaRecords.map(record => record.id);
const seqs = fastaRecords.map(record => record.seq);
alignSeqs(seqs).then((alignedSeqs: string[]) => {
const alignedFastaRecords = ids.map((id, index) => `${id}\n${alignedSeqs[index]}`);
const alignedDataset = alignedFastaRecords.join('\n');
console.log("AlignmentViewer alignedDataset: ", alignedDataset);
setDataset(alignedDataset);
setErrorMsg("No data");
setLoading(false);
}).catch((error) => {
console.error(error);
setDataset("");
setErrorMsg(error.message);
setLoading(false);
});
}
}, []);

return <Row className="alignment-viewer-container" style={{ marginTop: '20px', width: '100%', height: '100%' }}>
{
dataset === "" ?
<Empty description="No data" /> :
<ReactAlignmentViewer data={dataset} />
<Spin spinning={loading}>
<Empty description={errorMsg} />
</Spin> :
<ReactAlignmentViewer data={dataset} height={"500px"} />
}
</Row>
}
Expand Down
9 changes: 5 additions & 4 deletions studio/src/NodeInfoPanel/ComposedProteinPanel/index.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { Empty, Tabs, Spin } from "antd";
import { Empty, Tabs, Spin, Tag } 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 { guessSpecies, isExpectedSpecies, expectedTaxIdOrder, guessSpeciesAbbr } from '@/components/util';

import "./index.less";

Expand Down Expand Up @@ -88,7 +88,7 @@ const ComposedProteinPanel: React.FC<ComposedProteinPanel> = (props) => {
};
})
const orderedGeneInfos = oGeneInfos.sort((a, b) => {
return expectedOrder.indexOf(a.taxid.toString()) - expectedOrder.indexOf(b.taxid.toString());
return expectedTaxIdOrder.indexOf(a.taxid.toString()) - expectedTaxIdOrder.indexOf(b.taxid.toString());
});
setAllGeneInfos(orderedGeneInfos);

Expand Down Expand Up @@ -173,7 +173,8 @@ const ComposedProteinPanel: React.FC<ComposedProteinPanel> = (props) => {
}
});
oItems.push({
label: 'Alignment',
// @ts-ignore, we don't care about the warning. We need it to be a Tag component.
label: <Tag color="blue" style={{ fontSize: '14px', fontWeight: 'bold' }}>Alignment</Tag>,
key: oItems.length + 1,
children: <AlignmentViewer data={alignmentData} />
})
Expand Down
24 changes: 14 additions & 10 deletions studio/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { RequestConfig, history, RuntimeConfig } from 'umi';
import { PageLoading, SettingDrawer } from '@ant-design/pro-components';
import { Auth0Provider } from '@auth0/auth0-react';
import { CustomSettings, AppVersion } from '../config/defaultSettings';
import { getJwtAccessToken } from '@/components/util';
import { getJwtAccessToken, logout } from '@/components/util';
// import * as Sentry from "@sentry/react";

// Configure Sentry for error tracking
Expand Down Expand Up @@ -51,12 +51,18 @@ console.log('apiPrefix', process.env, apiPrefix);
const getUsername = (): string | undefined => {
const accessToken = getJwtAccessToken();
if (accessToken) {
const payload = accessToken.split('.')[1];
const base64 = payload.replace(/-/g, '+').replace(/_/g, '/');
const padLength = 4 - (base64.length % 4);
const paddedBase64 = padLength < 4 ? base64 + "=".repeat(padLength) : base64;
const payloadJson = JSON.parse(atob(paddedBase64));
return payloadJson['username'];
try {
const payload = accessToken.split('.')[1];
const base64 = payload.replace(/-/g, '+').replace(/_/g, '/');
const padLength = 4 - (base64.length % 4);
const paddedBase64 = padLength < 4 ? base64 + "=".repeat(padLength) : base64;
const payloadJson = JSON.parse(atob(paddedBase64));
return payloadJson['username'];
} catch (error) {
logout();
console.log('Error in getUsername: ', error);
return undefined;
}
} else {
return undefined;
}
Expand Down Expand Up @@ -198,9 +204,7 @@ export const layout: RuntimeConfig = (initialState: any) => {
},
links: [],
logout: () => {
localStorage.removeItem('rapex-visitor-id');
localStorage.removeItem('jwt_access_token');
localStorage.removeItem('redirectUrl');
logout();
history.push('/login');
},
// menuHeaderRender: false,
Expand Down
21 changes: 20 additions & 1 deletion studio/src/components/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export const guessColor = (text: string): string => {
return colors[text] || "#108ee9";
}

export const expectedOrder: string[] = [
export const expectedTaxIdOrder: string[] = [
'9606',
'10090',
'10116',
Expand All @@ -47,7 +47,20 @@ export const expectedOrder: string[] = [
'9598'
]

export const expectedSpeciesOrder: string[] = [
'Human',
'Mouse',
'Rattus norvegicus',
'Rat',
'Macaca fascicularis',
'M.fascicularis',
'Macaca mulatta',
'M.mulatta',
'Chimpanzee'
]

export const expectedSpecies: Record<string, string[]> = {
// Full name, abbreviation
'9606': ['Human', 'Human'],
'10090': ['Mouse', 'Mouse'],
'10116': ['Rattus norvegicus', 'Rat'],
Expand All @@ -67,3 +80,9 @@ export const guessSpeciesAbbr = (taxid: string) => {
export const isExpectedSpecies = (taxid: string) => {
return expectedSpecies[`${taxid}`] ? true : false
}

export const logout = () => {
localStorage.removeItem('rapex-visitor-id');
localStorage.removeItem('jwt_access_token');
localStorage.removeItem('redirectUrl');
}
5 changes: 5 additions & 0 deletions studio/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5480,6 +5480,11 @@ biominer-components@0.3.18:
viewerjs "^1.11.6"
voca "^1.4.1"

biomsa@^0.3.3:
version "0.3.3"
resolved "https://registry.yarnpkg.com/biomsa/-/biomsa-0.3.3.tgz#03fa9b137d324f71584ffe5d6a9240b2c14b48fa"
integrity sha512-SmpIwTMBE0JKAo18OHCWvIaUcrjKvED/opAltSFhHVx9Pz9j5HcWSHNhUvaU1o3AKDT5NVrWgc0Amp0OJRe+2w==

bit-twiddle@^1.0.0, bit-twiddle@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/bit-twiddle/-/bit-twiddle-1.0.2.tgz#0c6c1fabe2b23d17173d9a61b7b7093eb9e1769e"
Expand Down

0 comments on commit 8c3c695

Please sign in to comment.