Unverified Commit f667b98c authored by Andrey Azov's avatar Andrey Azov Committed by GitHub

Use thoas as data source for EntityViewer (#356)

parent 36226c7a
Pipeline #102740 passed with stages
in 13 minutes and 10 seconds
......@@ -66,8 +66,14 @@ export type EntityViewerParams = {
};
const client = new ApolloClient({
uri: '/toygraphql',
cache: new InMemoryCache()
uri: '/thoas',
cache: new InMemoryCache({
typePolicies: {
Gene: {
keyFields: ['stable_id']
}
}
})
});
const EntityViewer = (props: Props) => {
......
......@@ -33,7 +33,7 @@ import { ExampleFocusObject } from 'src/shared/state/genome/genomeTypes';
import styles from './ExampleLinks.scss';
type ExampleGene = {
id: string;
unversioned_stable_id: string;
symbol: string;
};
......@@ -43,9 +43,10 @@ type ExampleLinksProps = {
};
const QUERY = gql`
query Gene($id: String!) {
gene(byId: { id: $id }) {
id
query Gene($genomeId: String!, $geneId: String!) {
gene(byId: { genome_id: $genomeId, stable_id: $geneId }) {
stable_id
unversioned_stable_id
symbol
}
}
......@@ -56,9 +57,10 @@ const ExampleLinks = (props: ExampleLinksProps) => {
const exampleGeneId = props.exampleEntities.find(
({ type }) => type === 'gene'
)?.id;
const { activeGenomeId } = props;
const { loading, data, error } = useQuery<{ gene: ExampleGene }>(QUERY, {
variables: { id: exampleGeneId },
skip: !exampleGeneId
variables: { geneId: exampleGeneId, genomeId: activeGenomeId },
skip: !exampleGeneId || !activeGenomeId
});
if (loading) {
......@@ -79,7 +81,7 @@ const ExampleLinks = (props: ExampleLinksProps) => {
const featureIdInUrl = buildFocusIdForUrl({
type: 'gene',
objectId: data.gene.id
objectId: data.gene.unversioned_stable_id
});
const path = urlHelper.entityViewer({
genomeId: props.activeGenomeId,
......
......@@ -53,9 +53,10 @@ type GeneViewWithDataProps = {
};
const QUERY = gql`
query Gene($id: String!) {
gene(byId: { id: $id }) {
id
query Gene($genomeId: String!, $geneId: String!) {
gene(byId: { genome_id: $genomeId, stable_id: $geneId }) {
stable_id
unversioned_stable_id
version
slice {
location {
......@@ -69,14 +70,15 @@ const QUERY = gql`
}
}
transcripts {
id
stable_id
unversioned_stable_id
symbol
so_term: biotype
biotype
so_term
slice {
location {
start
end
length
}
region {
name
......@@ -85,17 +87,45 @@ const QUERY = gql`
}
}
}
exons {
slice {
location {
start
end
relative_location {
start
end
}
spliced_exons {
relative_location {
start
end
}
exon {
stable_id
slice {
location {
length
}
}
}
}
cds {
start
end
product_generating_contexts {
product_type
cds {
relative_start
relative_end
protein_length
}
cdna {
length
}
phased_exons {
start_phase
end_phase
exon {
stable_id
}
}
product {
stable_id
unversioned_stable_id
}
}
}
}
......@@ -104,11 +134,11 @@ const QUERY = gql`
const GeneView = () => {
const params: { [key: string]: string } = useParams();
const { entityId } = params;
const { genomeId, entityId } = params;
const { objectId: geneId } = parseFocusIdFromUrl(entityId);
const { loading, data } = useQuery<{ gene: Gene }>(QUERY, {
variables: { id: geneId }
variables: { geneId, genomeId }
});
// TODO decide about error handling
......
......@@ -71,10 +71,10 @@ const DefaultTranscriptslist = (props: Props) => {
<StripedBackground {...props} />
{sortedTranscripts.map((transcript, index) => {
const expandTranscript = props.expandedTranscriptIds.includes(
transcript.id
transcript.stable_id
);
const expandDownload = props.expandedTranscriptDownloadIds.includes(
transcript.id
transcript.stable_id
);
return (
......
......@@ -17,8 +17,6 @@
import React from 'react';
import { connect } from 'react-redux';
import { getFeatureCoordinates } from 'src/content/app/entity-viewer/shared/helpers/entity-helpers';
import UnsplicedTranscript from 'src/content/app/entity-viewer/gene-view/components/unspliced-transcript/UnsplicedTranscript';
import TranscriptsListItemInfo from '../transcripts-list-item-info/TranscriptsListItemInfo';
......@@ -54,12 +52,14 @@ export const DefaultTranscriptListItem = (
props: DefaultTranscriptListItemProps
) => {
const { scale } = props.rulerTicks;
const { start: geneStart } = getFeatureCoordinates(props.gene);
const { start: transcriptStart, end: transcriptEnd } = getFeatureCoordinates(
props.transcript
);
const transcriptStartX = scale(transcriptStart - geneStart); // FIXME In future, this should be done using relative position of transcript in gene
const transcriptWidth = scale(transcriptEnd - transcriptStart) as number; // FIXME this too should be based on relative coordinates of transcript
const {
relative_location: { start: relativeTranscriptStart },
slice: {
location: { length: transcriptLength }
}
} = props.transcript;
const transcriptStartX = scale(relativeTranscriptStart) as number;
const transcriptWidth = scale(transcriptLength) as number;
const defaultTranscriptLabelMap = {
selected: {
......@@ -85,7 +85,9 @@ export const DefaultTranscriptListItem = (
<div className={transcriptsListStyles.middle}>
<div
className={styles.clickableTranscriptArea}
onClick={() => props.toggleTranscriptInfo(props.transcript.id)}
onClick={() =>
props.toggleTranscriptInfo(props.transcript.stable_id)
}
>
<div
className={styles.transcriptWrapper}
......@@ -101,9 +103,11 @@ export const DefaultTranscriptListItem = (
</div>
<div
className={transcriptsListStyles.right}
onClick={() => props.toggleTranscriptInfo(props.transcript.id)}
onClick={() => props.toggleTranscriptInfo(props.transcript.stable_id)}
>
<span className={styles.transcriptId}>{props.transcript.id}</span>
<span className={styles.transcriptId}>
{props.transcript.stable_id}
</span>
</div>
</div>
{props.expandTranscript ? (
......
......@@ -69,13 +69,8 @@ describe('<TranscriptsListItemInfo /', () => {
* 2) we will check that protein product is present on a transcript instead of looking at CDS
*/
it('displays amino acid length when transcript has CDS', () => {
const totalExonsLength = defaultProps.transcript.exons.reduce(
(sum, exon) => {
return sum + exon.slice.location.end - exon.slice.location.start + 1;
},
0
);
const expectedProteinLength = Math.floor(totalExonsLength / 3);
const expectedProteinLength =
defaultProps.transcript.product_generating_contexts[0].product?.length;
expect(wrapper.find('.topMiddle strong').text()).toMatch(
`${expectedProteinLength}`
);
......
......@@ -22,11 +22,12 @@ import { connect } from 'react-redux';
import { getCommaSeparatedNumber } from 'src/shared/helpers/formatters/numberFormatter';
import { getFormattedLocation } from 'src/shared/helpers/formatters/regionFormatter';
import {
isProteinCodingTranscript,
getFeatureCoordinates,
getRegionName,
getFirstAndLastCodingExonIndexes,
getNumberOfCodingExons,
getSplicedRNALength
getSplicedRNALength,
getProductAminoAcidLength
} from 'src/content/app/entity-viewer/shared/helpers/entity-helpers';
import * as urlFor from 'src/shared/helpers/urlHelper';
import { buildFocusIdForUrl } from 'src/shared/state/ens-object/ensObjectHelpers';
......@@ -67,50 +68,6 @@ export const TranscriptsListItemInfo = (
});
};
// FIXME: remove this when the amino acid length can be retrieved via the API
const getAminoAcidLength = () => {
const { exons, cds } = transcript;
if (cds) {
const {
firstCodingExonIndex,
lastCodingExonIndex
} = getFirstAndLastCodingExonIndexes(transcript);
if (firstCodingExonIndex === lastCodingExonIndex) {
return Math.floor((cds.end - cds.start + 1) / 3);
}
let cdsLength = 0;
// add coding length of the first coding exon
const { end: firstCodingExonEnd } = getFeatureCoordinates(
exons[firstCodingExonIndex]
);
cdsLength += firstCodingExonEnd - cds.start + 1;
// add coding length of the last coding exon
const { start: lastCodingExonStart } = getFeatureCoordinates(
exons[lastCodingExonIndex]
);
cdsLength += cds.end - lastCodingExonStart + 1;
// add coding length of exons between first and last coding exons
for (
let index = firstCodingExonIndex + 1;
index <= lastCodingExonIndex - 1;
index += 1
) {
const { start, end } = getFeatureCoordinates(exons[index]);
cdsLength += end - start + 1;
}
const aminoAcidLength = Math.floor(cdsLength / 3);
return aminoAcidLength;
} else {
return 0;
}
};
const splicedRNALength = getCommaSeparatedNumber(
getSplicedRNALength(transcript)
);
......@@ -120,7 +77,7 @@ export const TranscriptsListItemInfo = (
const focusIdForUrl = buildFocusIdForUrl({
type: 'gene',
objectId: props.gene.id
objectId: props.gene.unversioned_stable_id
});
const getBrowserLink = () => {
......@@ -133,37 +90,45 @@ export const TranscriptsListItemInfo = (
<div className={midStyles}>
<div className={styles.topLeft}>
<div>
<strong>{transcript.biotype}</strong>
<strong>{transcript.so_term}</strong>
</div>
<div>{getTranscriptLocation()}</div>
</div>
<div className={styles.topMiddle}>
{transcript.cds && (
{isProteinCodingTranscript(transcript) && (
<>
<div>
<strong>{getAminoAcidLength()} aa</strong>
<strong>{getProductAminoAcidLength(transcript)} aa</strong>
</div>
<div>
{transcript.product_generating_contexts[0]?.product.stable_id}
</div>
<div>ENSP1000000000</div>
</>
)}
</div>
<div className={styles.topRight}>
<div>
Spliced RNA length <strong>{splicedRNALength} </strong> bp
Combined exon length <strong>{splicedRNALength}</strong> bp
</div>
<div>
Coding exons <strong>{getNumberOfCodingExons(transcript)}</strong>{' '}
of {transcript.exons.length}
of {transcript.spliced_exons.length}
</div>
</div>
<div className={styles.downloadLink}>
{props.expandDownload ? (
<CloseIcon
className={styles.closeIcon}
onClick={() => props.toggleTranscriptDownload(transcript.id)}
onClick={() =>
props.toggleTranscriptDownload(transcript.stable_id)
}
/>
) : (
<span onClick={() => props.toggleTranscriptDownload(transcript.id)}>
<span
onClick={() =>
props.toggleTranscriptDownload(transcript.stable_id)
}
>
Download
</span>
)}
......@@ -188,7 +153,13 @@ const renderInstantDownload = ({
}: TranscriptsListItemInfoProps) => {
return (
<div className={styles.download}>
<InstantDownloadTranscript transcript={transcript} gene={gene} />
<InstantDownloadTranscript
transcript={{
id: transcript.unversioned_stable_id,
so_term: transcript.so_term
}}
gene={{ id: gene.unversioned_stable_id }}
/>
</div>
);
};
......
......@@ -29,6 +29,7 @@ import {
} from 'src/content/app/entity-viewer/state/gene-view/view/geneViewViewSlice';
import * as urlFor from 'src/shared/helpers/urlHelper';
import { isProteinCodingTranscript } from 'src/content/app/entity-viewer/shared/helpers/entity-helpers';
import Tabs, { Tab } from 'src/shared/components/tabs/Tabs';
import Panel from 'src/shared/components/panel/Panel';
......@@ -85,14 +86,13 @@ const GeneFunction = (props: Props) => {
};
// Check if we have at least one protein coding transcript
// TODO: use a more reliable indicator than the biotype field
const isProteinCodingTranscript = transcripts.some(
(transcript) => transcript.biotype === 'protein_coding'
const hasProteinCodingTranscripts = transcripts.some(
isProteinCodingTranscript
);
// Disable the Proteins tab if there are no transcripts data
// TODO: We need a better logic to disable tabs once we have the data available for other tabs
if (!isProteinCodingTranscript) {
if (!hasProteinCodingTranscripts) {
const proteinTabIndex = tabsData.findIndex(
(tab) => tab.title === GeneFunctionTabName.PROTEINS
);
......@@ -114,7 +114,7 @@ const GeneFunction = (props: Props) => {
const getCurrentTabContent = () => {
switch (selectedTabName) {
case GeneFunctionTabName.PROTEINS:
return <ProteinsList geneId={props.gene.id} />;
return <ProteinsList gene={props.gene} />;
default:
return <>Data for these views will be available soon...</>;
}
......
......@@ -97,9 +97,7 @@ export const GeneImage = (props: GeneOverviewImageProps) => {
};
const GeneId = (props: GeneOverviewImageProps) => (
<div className={styles.geneId}>
{props.gene.id}.{props.gene.version}
</div>
<div className={styles.geneId}>{props.gene.stable_id}</div>
);
const DirectionIndicator = () => {
......
......@@ -16,7 +16,6 @@
import React from 'react';
import { connect } from 'react-redux';
import { ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client';
import GeneOverview from 'src/content/app/entity-viewer/gene-view/components/gene-view-sidebar/overview/GeneOverview';
import GeneExternalReferences from 'src/content/app/entity-viewer/gene-view/components/gene-view-sidebar/external-references/GeneExternalReferences';
......@@ -30,19 +29,14 @@ type Props = {
activeTabName: SidebarTabName | null;
};
const client = new ApolloClient({
uri: '/thoas',
cache: new InMemoryCache()
});
const GeneViewSidebar = (props: Props) => {
return (
<ApolloProvider client={client}>
<>
{props.activeTabName === SidebarTabName.OVERVIEW && <GeneOverview />}
{props.activeTabName === SidebarTabName.EXTERNAL_REFERENCES && (
<GeneExternalReferences />
)}
</ApolloProvider>
</>
);
};
......
......@@ -26,7 +26,7 @@ import ProteinDomainImage, {
const product = createProduct();
const minimalProps = {
proteinDomains: product.protein_domains_resources,
proteinDomains: product.protein_domains,
width: 600
};
......
......@@ -18,7 +18,7 @@ import React from 'react';
import classNames from 'classnames';
import { scaleLinear, ScaleLinear } from 'd3';
import { ProteinDomainsResources } from 'src/content/app/entity-viewer/types/product';
import { ProteinDomain } from 'src/content/app/entity-viewer/types/product';
import styles from './ProteinDomainImage.scss';
......@@ -26,7 +26,7 @@ const BLOCK_HEIGHT = 18;
const TRACK_HEIGHT = 24;
export type ProteinDomainImageProps = {
proteinDomains: ProteinDomainsResources;
proteinDomains: ProteinDomain[];
trackLength: number;
width: number; // available width for drawing, in pixels
classNames?: {
......@@ -44,32 +44,31 @@ type ProteinDomainImageData = {
};
};
export const getDomainsByResourceGroups = (
proteinDomainsResources: ProteinDomainsResources
) => {
const proteinDomains: ProteinDomainImageData = {};
export const getDomainsByResourceGroups = (proteinDomains: ProteinDomain[]) => {
const groupedDomains: ProteinDomainImageData = {};
Object.keys(proteinDomainsResources).forEach((resourceName) => {
if (!proteinDomains[resourceName]) {
proteinDomains[resourceName] = {};
}
proteinDomains.forEach((domain) => {
const {
resource_name,
name: domainName,
location: { start, end }
} = domain;
proteinDomainsResources[resourceName].domains.forEach((domain) => {
const domainName = domain.name;
const { start, end } = domain.location;
if (!groupedDomains[resource_name]) {
groupedDomains[resource_name] = {};
}
if (!proteinDomains[resourceName][domainName]) {
proteinDomains[resourceName][domainName] = [];
}
if (!groupedDomains[resource_name][domainName]) {
groupedDomains[resource_name][domainName] = [];
}
proteinDomains[resourceName][domainName].push({
start: start,
end: end
});
groupedDomains[resource_name][domainName].push({
start,
end
});
});
return proteinDomains;
return groupedDomains;
};
const ProteinDomainImage = (props: ProteinDomainImageProps) => {
......
......@@ -14,12 +14,14 @@
* limitations under the License.
*/
import React, { useEffect, useState } from 'react';
import React from 'react';
import ProteinsListItem from './proteins-list-item/ProteinsListItem';
import { fetchGene } from 'src/content/app/entity-viewer/shared/rest/rest-data-fetchers/geneData';
import { getLongestProteinLength } from 'src/content/app/entity-viewer/shared/helpers/entity-helpers';
import {
getLongestProteinLength,
isProteinCodingTranscript
} from 'src/content/app/entity-viewer/shared/helpers/entity-helpers';
import { defaultSort } from 'src/content/app/entity-viewer/shared/helpers/transcripts-sorter';
import { Gene } from 'src/content/app/entity-viewer/types/gene';
......@@ -27,37 +29,13 @@ import { Gene } from 'src/content/app/entity-viewer/types/gene';