Unverified Commit afb88c7c authored by Imran Salam's avatar Imran Salam Committed by GitHub
Browse files

Add UniProt and PDBe IDs (#314)

* add uniprot and pdbe ids

* refactor data fetching for protein list item info

* use shared component for displaying external references

* refactor ProteinFeaturesCount

* more PR suggestions

* more PR suggestions
parent 933096ee
Pipeline #84164 passed with stages
in 8 minutes and 11 seconds
......@@ -19,8 +19,7 @@ import { mount } from 'enzyme';
import { createProduct } from 'tests/fixtures/entity-viewer/product';
import {
ProteinDomainImageWithData as ProteinDomainImage,
import ProteinDomainImage, {
getDomainsByResourceGroups
} from './ProteinDomainImage';
......
......@@ -14,16 +14,11 @@
* limitations under the License.
*/
import React, { useState, useEffect } from 'react';
import React from 'react';
import classNames from 'classnames';
import { scaleLinear, ScaleLinear } from 'd3';
import { fetchTranscript } from 'src/content/app/entity-viewer/shared/rest/rest-data-fetchers/transcriptData';
import {
ProteinDomainsResources,
Product
} from 'src/content/app/entity-viewer/types/product';
import { ProteinDomainsResources } from 'src/content/app/entity-viewer/types/product';
import styles from './ProteinDomainImage.scss';
......@@ -31,7 +26,7 @@ const BLOCK_HEIGHT = 18;
const TRACK_HEIGHT = 24;
export type ProteinDomainImageProps = {
transcriptId: string;
proteinDomains: ProteinDomainsResources;
trackLength: number;
width: number; // available width for drawing, in pixels
classNames?: {
......@@ -40,13 +35,6 @@ export type ProteinDomainImageProps = {
};
};
type ProteinDomainImageWithDataProps = Omit<
ProteinDomainImageProps,
'transcriptId'
> & {
proteinDomains: ProteinDomainsResources;
};
type ProteinDomainImageData = {
[resource_type: string]: {
[resource_description: string]: {
......@@ -85,37 +73,7 @@ export const getDomainsByResourceGroups = (
};
const ProteinDomainImage = (props: ProteinDomainImageProps) => {
const [product, setProduct] = useState<Product | null>(null);
useEffect(() => {
const abortController = new AbortController();
fetchTranscript(props.transcriptId, abortController.signal).then(
(result) => {
if (result?.product) {
setProduct(result.product);
}
}
);
return function cleanup() {
abortController.abort();
};
}, [props.transcriptId]);
return product?.protein_domains_resources ? (
<ProteinDomainImageWithData
{...props}
proteinDomains={product.protein_domains_resources}
/>
) : null;
};
export const ProteinDomainImageWithData = (
props: ProteinDomainImageWithDataProps
) => {
const { proteinDomains, trackLength } = props;
const proteinDomainsResources = getDomainsByResourceGroups(proteinDomains);
// Create a scale where the domain is the total length of the track in amino acids.
......
......@@ -14,82 +14,71 @@
* limitations under the License.
*/
import React, { useState, useEffect } from 'react';
import classNames from 'classnames';
import React from 'react';
import {
ProteinStats,
fetchProteinSummaryStats
} from 'src/content/app/entity-viewer/shared/rest/rest-data-fetchers/proteinData';
import { ProteinStats } from 'src/content/app/entity-viewer/shared/rest/rest-data-fetchers/proteinData';
import structuresIcon from 'static/img/entity-viewer/icon_protein_structures.svg';
import ligandsIcon from 'static/img/entity-viewer/icon_protein_ligands.svg';
import interactionsIcon from 'static/img/entity-viewer/icon_protein_interactions.svg';
import annotationsIcon from 'static/img/entity-viewer/icon_protein_annotations.svg';
import transcriptListStyles from 'src/content/app/entity-viewer/gene-view/components/default-transcripts-list/DefaultTranscriptsList.scss';
import styles from './ProteinFeaturesCount.scss';
type ProteinFeaturesCountProps = {
transcriptId: string;
proteinStats: ProteinStats;
};
const ProteinFeaturesCount = (props: ProteinFeaturesCountProps) => {
const [proteinStats, setProteinStats] = useState<ProteinStats | null>(null);
useEffect(() => {
fetchProteinSummaryStats(props.transcriptId).then((response) => {
setProteinStats(response);
});
}, [props.transcriptId]);
enum FeatureCountLabel {
ANNOTATIONS = 'Functional annotations',
INTERACTIONS = 'Interactions',
LIGANDS = 'Ligands',
STRUCTURES = 'Structures'
}
if (!proteinStats) {
return null;
}
const midStyles = classNames(transcriptListStyles.middle, styles.middle);
const ProteinFeaturesCount = (props: ProteinFeaturesCountProps) => {
const { proteinStats } = props;
return (
<div className={transcriptListStyles.row}>
<div className={transcriptListStyles.left}>PDBe-KB P51587</div>
<div className={midStyles}>
<div className={styles.feature}>
<div className={styles.featureImg}>
<img src={structuresIcon} alt="" />
</div>
<div className={styles.featureCount}>
{proteinStats.structuresCount}
</div>
<div className={styles.featureText}>Structures</div>
</div>
<div className={styles.feature}>
<div className={styles.featureImg}>
<img src={ligandsIcon} alt="" />
</div>
<div className={styles.featureCount}>{proteinStats.ligandsCount}</div>
<div className={styles.featureText}>Ligands</div>
</div>
<div className={styles.feature}>
<div className={styles.featureImg}>
<img src={interactionsIcon} alt="" />
</div>
<div className={styles.featureCount}>
{proteinStats.interactionsCount}
</div>
<div className={styles.featureText}>Interactions</div>
</div>
<div className={styles.feature}>
<div className={styles.featureImg}>
<img src={annotationsIcon} alt="" />
</div>
<div className={styles.featureCount}>
{proteinStats.annotationsCount}
</div>
<div className={styles.featureText}>Functional annotations</div>
</div>
</div>
<div>
<FeatureCount
label={FeatureCountLabel.STRUCTURES}
count={proteinStats.structuresCount}
icon={structuresIcon}
/>
<FeatureCount
label={FeatureCountLabel.LIGANDS}
count={proteinStats.ligandsCount}
icon={ligandsIcon}
/>
<FeatureCount
label={FeatureCountLabel.INTERACTIONS}
count={proteinStats.interactionsCount}
icon={interactionsIcon}
/>
<FeatureCount
label={FeatureCountLabel.ANNOTATIONS}
count={proteinStats.annotationsCount}
icon={annotationsIcon}
/>
</div>
);
};
type FeatureCountProps = {
count: number;
icon: string;
label: string;
};
const FeatureCount = (props: FeatureCountProps) => (
<div className={styles.feature}>
<div className={styles.featureImg}>
<img src={props.icon} alt={props.label} />
</div>
<div className={styles.featureCount}>{props.count}</div>
<div className={styles.featureText}>{props.label}</div>
</div>
);
export default ProteinFeaturesCount;
......@@ -14,13 +14,10 @@
* limitations under the License.
*/
import React, { useState, useEffect } from 'react';
import React from 'react';
import { scaleLinear } from 'd3';
import classNames from 'classnames';
import { fetchTranscript } from 'src/content/app/entity-viewer/shared/rest/rest-data-fetchers/transcriptData';
import { Transcript } from 'src/content/app/entity-viewer/types/transcript';
import { Product } from 'src/content/app/entity-viewer/types/product';
import transcriptsListStyles from 'src/content/app/entity-viewer/gene-view/components/default-transcripts-list/DefaultTranscriptsList.scss';
......@@ -30,46 +27,13 @@ const TRACK_HEIGHT = 24;
const PROTEIN_HEIGHT = 10;
type ProteinImageProps = {
transcriptId: string;
product: Product;
trackLength: number; // length in amino acids
className?: string;
width: number; // available width for drawing in pixels
};
type ProteinImageWithDataProps = Omit<ProteinImageProps, 'transcriptId'> & {
product: Product;
};
export const ProteinImage = (props: ProteinImageProps) => {
const [transcript, setTranscript] = useState<Transcript | null>(null);
useEffect(() => {
const abortController = new AbortController();
fetchTranscript(props.transcriptId, abortController.signal).then(
(result) => {
if (result) {
setTranscript(result);
}
}
);
return function cleanup() {
abortController.abort();
};
}, [props.transcriptId]);
return transcript?.product ? (
<ProteinImageWithData
product={transcript.product}
trackLength={props.trackLength}
className={props.className}
width={props.width}
/>
) : null;
};
const ProteinImageWithData = (props: ProteinImageWithDataProps) => {
const ProteinImage = (props: ProteinImageProps) => {
// Create a scale where the domain is the total length of the track in amino acids.
// The track is as wide as the longest protein generated from the gene.
// Therefore, it is guaranteed that the length of the protein drawn by this component will fall within this domain.
......
.bottomWrapper {
display: grid;
grid-template-columns: 200px 20px 475px;
margin: 10px 0 10px 178px;
margin-left: 178px;
width: 695px;
> div {
margin: 10px 0 10px 0;
}
}
.codingExonCount {
grid-column: 1/2;
}
.sequenceDownload {
grid-column: 3/4;
text-align: right;
.geneExternalReference {
display: inline-block;
margin-right: 40px;
}
.proteinFeaturesCountWrapper {
display: inline-block;
}
......@@ -14,45 +14,120 @@
* limitations under the License.
*/
import React from 'react';
import React, { useEffect, useState } from 'react';
import ProteinDomainImage from 'src/content/app/entity-viewer/gene-view/components/protein-domain-image/ProteinDomainImage';
import ProteinImage from 'src/content/app/entity-viewer/gene-view/components/protein-image/ProteinImage';
import ProteinFeaturesCount from 'src/content/app/entity-viewer/gene-view/components/protein-features-count/ProteinFeaturesCount';
import ExternalReference from 'src/shared/components/external-reference/ExternalReference';
import {
ExternalSource,
externalSourceLinks
} from 'src/content/app/entity-viewer/shared/helpers/entity-helpers';
import { fetchTranscript } from 'src/content/app/entity-viewer/shared/rest/rest-data-fetchers/transcriptData';
import {
fetchProteinSummary,
ProteinSummary
} from 'src/content/app/entity-viewer/shared/rest/rest-data-fetchers/proteinData';
import { Transcript } from 'src/content/app/entity-viewer/types/transcript';
import styles from './ProteinsListItemInfo.scss';
type Props = {
transcript: Transcript;
transcriptId: string;
trackLength: number;
};
const ProteinsListItemInfo = (props: Props) => {
const { transcript, trackLength } = props;
const { transcriptId, trackLength } = props;
const [transcript, setTranscript] = useState<Transcript | null>(null);
const [proteinSummary, setProteinSummary] = useState<ProteinSummary | null>(
null
);
useEffect(() => {
const abortController = new AbortController();
Promise.all([
fetchTranscript(props.transcriptId, abortController.signal),
fetchProteinSummary(props.transcriptId, abortController.signal)
]).then(([transcriptData, proteinSummaryData]) => {
transcriptData && setTranscript(transcriptData);
proteinSummaryData && setProteinSummary(proteinSummaryData);
});
return function cleanup() {
abortController.abort();
};
}, [transcriptId]);
return (
<div className={styles.proteinsListItemInfo}>
{transcript.cds && (
<ProteinDomainImage
transcriptId={transcript.id}
trackLength={trackLength}
width={695}
/>
)}
{transcript.cds && (
{transcript?.product && (
<>
<ProteinDomainImage
proteinDomains={transcript.product?.protein_domains_resources}
trackLength={trackLength}
width={695}
/>
<ProteinImage
transcriptId={transcript.id}
product={transcript.product}
trackLength={trackLength}
width={695}
/>
<ProteinFeaturesCount transcriptId={transcript.id} />
</>
)}
{proteinSummary && (
<div className={styles.bottomWrapper}>
<div>
<ProteinExternalReference
source={ExternalSource.INTERPRO}
externalId={proteinSummary.pdbeId}
/>
<ProteinExternalReference
source={ExternalSource.UNIPROT}
externalId={proteinSummary.pdbeId}
/>
Download component
</div>
<div>
<ProteinExternalReference
source={ExternalSource.PDBE}
externalId={proteinSummary.pdbeId}
/>
{proteinSummary?.proteinStats && (
<div className={styles.proteinFeaturesCountWrapper}>
<ProteinFeaturesCount
proteinStats={proteinSummary.proteinStats}
/>
</div>
)}
</div>
</div>
)}
</div>
);
};
type ProteinExternalReferenceProps = {
source: ExternalSource;
externalId: string | undefined;
};
const ProteinExternalReference = (props: ProteinExternalReferenceProps) => {
const url = `${externalSourceLinks[props.source]}${props.externalId}`;
return props.externalId ? (
<div className={styles.geneExternalReference}>
<ExternalReference
label={props.source}
to={url}
linkText={props.externalId}
/>
</div>
) : null;
};
export default ProteinsListItemInfo;
......@@ -55,7 +55,7 @@ const ProteinsListItem = (props: Props) => {
</div>
{shouldShowInfo ? (
<ProteinsListItemInfo
transcript={transcript}
transcriptId={transcript.id}
trackLength={trackLength}
/>
) : null}
......
......@@ -89,3 +89,15 @@ export const getLongestProteinLength = (gene: Gene) => {
return Math.max(...proteinLengths);
};
export enum ExternalSource {
INTERPRO = 'Interpro',
UNIPROT = 'UniProt',
PDBE = 'PDBe-KB'
}
export const externalSourceLinks = {
[ExternalSource.INTERPRO]: 'https://www.ebi.ac.uk/interpro/protein/UniProt/',
[ExternalSource.UNIPROT]: 'https://www.uniprot.org/uniprot/',
[ExternalSource.PDBE]: 'https://www.ebi.ac.uk/pdbe/pdbe-kb/proteins/'
};
......@@ -16,14 +16,18 @@
import {
ProteinStatsInResponse,
ProteinStats
ProteinSummary
} from '../rest-data-fetchers/proteinData';
export const restProteinStatsAdaptor = (
proteinStats: ProteinStatsInResponse
): ProteinStats => ({
structuresCount: proteinStats.pdbs,
ligandsCount: proteinStats.ligands,
interactionsCount: proteinStats.interaction_partners,
annotationsCount: proteinStats.annotations
export const restProteinSummaryAdaptor = (
proteinStats: ProteinStatsInResponse,
pdbeId: string
): ProteinSummary => ({
proteinStats: {
structuresCount: proteinStats.pdbs,
ligandsCount: proteinStats.ligands,
interactionsCount: proteinStats.interaction_partners,
annotationsCount: proteinStats.annotations
},
pdbeId
});
......@@ -14,12 +14,14 @@
* limitations under the License.
*/
import { restProteinStatsAdaptor } from '../rest-adaptors/rest-protein-adaptor';
import { restProteinSummaryAdaptor } from '../rest-adaptors/rest-protein-adaptor';
import { TranscriptInResponse } from './transcriptData';
export type XrefsInResponse = {
export type Xref = {
display_id: string;
}[];
};
export type XrefsInResponse = Xref[];
export type ProteinStatsInResponse = {
pdbs: number;
......@@ -39,10 +41,15 @@ export type ProteinStats = {
annotationsCount: number;
};
export const fetchProteinSummaryStats = async (
export type ProteinSummary = {
proteinStats: ProteinStats;
pdbeId: string;
};
export const fetchProteinSummary = async (
transcriptId: string,
signal?: AbortSignal
): Promise<ProteinStats | null> => {
): Promise<ProteinSummary | null> => {
const transcriptUrl = `https://rest.ensembl.org/lookup/id/${transcriptId}?expand=1;content-type=application/json`;
const transcript: TranscriptInResponse = await fetch(transcriptUrl, {
signal
......@@ -61,7 +68,7 @@ export const fetchProteinSummaryStats = async (
signal
}).then((response) => response.json());
return restProteinStatsAdaptor(proteinStatsData[pdbeId]);
return restProteinSummaryAdaptor(proteinStatsData[pdbeId], pdbeId);
} else {
return null;
}
......
......@@ -19,7 +19,7 @@ import { storiesOf } from '@storybook/react';