Unverified Commit 58aa77a2 authored by Jyothish's avatar Jyothish Committed by GitHub
Browse files

Label Ensembl canonical on Transcript Image using data from Thoas (#520)

Plus updated the default sorting rules.
parent 7214c8c0
Pipeline #174542 passed with stages
in 5 minutes and 3 seconds
......@@ -155,6 +155,18 @@ const QUERY = gql`
}
}
}
metadata {
canonical {
value
label
definition
}
mane {
value
label
definition
}
}
}
}
}
......
......@@ -14,15 +14,12 @@
* limitations under the License.
*/
import React, { useEffect, useMemo } from 'react';
import React, { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { Pick2, Pick3 } from 'ts-multipick';
import { getFeatureCoordinates } from 'src/content/app/entity-viewer/shared/helpers/entity-helpers';
import {
transcriptSortingFunctions,
defaultSort
} from 'src/content/app/entity-viewer/shared/helpers/transcripts-sorter';
import { transcriptSortingFunctions } from 'src/content/app/entity-viewer/shared/helpers/transcripts-sorter';
import { filterTranscriptsBySOTerm } from 'src/content/app/entity-viewer/shared/helpers/transcripts-filter';
......@@ -58,6 +55,7 @@ type Transcript = DefaultTranscriptListItemProps['transcript'] & {
} & {
spliced_exons: Array<Pick3<SplicedExon, 'exon', 'slice', 'location'>>;
} & Pick2<FullTranscript, 'slice', 'location'>;
type Gene = DefaultTranscriptListItemProps['gene'] & {
stable_id: FullGene['stable_id'];
transcripts: Array<Transcript>;
......@@ -80,12 +78,6 @@ const DefaultTranscriptslist = (props: Props) => {
const { gene } = props;
//Using this to get the default order of transcripts in which the first one is selected, this might change later with the data coming directly from thoas
const defaultTranscriptId = useMemo(() => {
const sortedTranscripts = defaultSort(gene.transcripts);
return sortedTranscripts[0].stable_id;
}, [gene.stable_id]);
const sortingFunction = transcriptSortingFunctions[sortingRule];
const sortedTranscripts = sortingFunction(gene.transcripts) as Transcript[];
const filteredTranscripts = Object.values(filters).some(Boolean)
......@@ -121,7 +113,6 @@ const DefaultTranscriptslist = (props: Props) => {
return (
<DefaultTranscriptsListItem
key={index}
isDefault={transcript.stable_id === defaultTranscriptId}
gene={gene}
transcript={transcript}
rulerTicks={props.rulerTicks}
......
......@@ -87,11 +87,4 @@ describe('<DefaultTranscriptListItem />', () => {
expect(queryByTestId('transcriptsListItemInfo')).toBeTruthy();
});
it('displays selected transcript', () => {
const { container } = renderComponent({ isDefault: true });
expect(
container.querySelector('.transcriptQualityLabel')?.textContent
).toBe('Selected');
});
});
......@@ -33,7 +33,10 @@ import { TranscriptQualityLabel } from 'src/content/app/entity-viewer/shared/com
import transcriptsListStyles from '../DefaultTranscriptsList.scss';
import styles from './DefaultTranscriptListItem.scss';
type Transcript = Pick<FullTranscript, 'stable_id' | 'relative_location'> &
type Transcript = Pick<
FullTranscript,
'stable_id' | 'relative_location' | 'metadata'
> &
TranscriptsListItemInfoProps['transcript'] &
UnsplicedTranscriptProps['transcript'];
......@@ -63,7 +66,7 @@ export const DefaultTranscriptListItem = (
return (
<div className={styles.defaultTranscriptListItem}>
<div className={transcriptsListStyles.row}>
{props.isDefault && <TranscriptQualityLabel />}
<TranscriptQualityLabel metadata={props.transcript.metadata} />
<div className={transcriptsListStyles.middle}>
<div
......
......@@ -39,6 +39,7 @@ import {
import { EntityViewerParams } from 'src/content/app/entity-viewer/EntityViewer';
import { Slice } from 'src/shared/types/thoas/slice';
import { FullProductGeneratingContext } from 'src/shared/types/thoas/productGeneratingContext';
import { TranscriptMetadata } from 'ensemblRoot/src/shared/types/thoas/metadata';
import styles from './GeneExternalReferences.scss';
......@@ -90,6 +91,14 @@ const QUERY = gql`
}
}
}
metadata {
canonical {
value
}
mane {
value
}
}
}
}
}
......@@ -105,6 +114,7 @@ type Transcript = {
}
>;
external_references: ExternalReferenceType[];
metadata: Pick<TranscriptMetadata, 'canonical' | 'mane'>;
};
type Gene = {
......
......@@ -53,6 +53,7 @@ type Product = Pick<
};
type Transcript = Pick<FullTranscript, 'stable_id'> &
Pick2<FullTranscript, 'metadata', 'mane' | 'canonical'> &
ProteinsListItemInfoProps['transcript'] & {
product_generating_contexts: Array<
Pick<FullProductGeneratingContext, 'product_type'> & {
......@@ -68,7 +69,7 @@ export type Props = {
};
const ProteinsListItem = (props: Props) => {
const { isDefault, transcript, trackLength } = props;
const { transcript, trackLength } = props;
const expandedTranscriptIds = useSelector(getExpandedTranscriptIds);
const dispatch = useDispatch();
......@@ -124,7 +125,7 @@ const ProteinsListItem = (props: Props) => {
<span className={styles.scrollRef} ref={itemRef}></span>
<div className={transcriptsListStyles.row}>
<div className={transcriptsListStyles.left}>
{isDefault && <TranscriptQualityLabel />}
<TranscriptQualityLabel metadata={transcript.metadata} />
</div>
<div onClick={toggleListItemInfo} className={midStyles}>
<div>{getProductAminoAcidLength(transcript)} aa</div>
......
/**
* See the NOTICE file distributed with this work for additional information
* regarding copyright ownership.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import faker from 'faker';
import { render } from '@testing-library/react';
import { TranscriptQualityLabel } from './TranscriptQualityLabel';
const metadata = {
canonical: {
label: faker.lorem.word(),
value: true,
definition: faker.lorem.sentence()
},
mane: {
label: faker.lorem.word(),
value: faker.lorem.word(),
definition: faker.lorem.sentence()
}
};
describe('<TranscriptQualityLabel />', () => {
beforeEach(() => {
jest.resetAllMocks();
});
it('displays correct labels for transcript metadata', () => {
const { container, queryByText, rerender } = render(
<TranscriptQualityLabel metadata={metadata} />
);
let label = queryByText(metadata.mane.label);
expect(label).toBeTruthy();
rerender(<TranscriptQualityLabel metadata={{ ...metadata, mane: null }} />);
label = queryByText(metadata.canonical.label);
expect(label).toBeTruthy();
rerender(
<TranscriptQualityLabel metadata={{ ...metadata, canonical: null }} />
);
label = queryByText(metadata.mane.label);
expect(label).toBeTruthy();
rerender(
<TranscriptQualityLabel
metadata={{ ...metadata, canonical: null, mane: null }}
/>
);
expect(container.firstChild).toBe(null);
});
});
......@@ -18,31 +18,44 @@ import React from 'react';
import QuestionButton from 'src/shared/components/question-button/QuestionButton';
import { TranscriptMetadata } from 'ensemblRoot/src/shared/types/thoas/metadata';
import styles from './TranscriptQualityLabel.scss';
const transcriptLabelMap = {
selected: {
label: 'Selected',
helpText:
'The selected transcript is a default single transcript per protein coding gene that is representative of biology, well-supported, expressed and highly conserved'
}
type Props = {
metadata: Pick<TranscriptMetadata, 'canonical' | 'mane'>;
};
export const TranscriptQualityLabel = () => {
// TODO show more meaningful labels by using transcript metadata when the api starts returning it
const transcriptQuality = 'selected';
const getTranscriptMetadata = (props: Props) => {
const { canonical, mane } = props.metadata;
if (canonical && mane) {
return {
label: mane.label,
definition: mane.definition
};
} else if (canonical) {
return {
label: canonical.label,
definition: canonical.definition
};
} else if (mane) {
return {
label: mane.label,
definition: mane.definition
};
}
};
const labelText = transcriptLabelMap[transcriptQuality]?.label;
if (!labelText) {
export const TranscriptQualityLabel = (props: Props) => {
const metadata = getTranscriptMetadata(props);
if (!metadata) {
return null;
}
return (
<div className={styles.transcriptQualityLabel}>
<span>{labelText}</span>
<QuestionButton
helpText={transcriptLabelMap[transcriptQuality]?.helpText}
/>
<span>{metadata.label}</span>
<QuestionButton helpText={metadata.definition} />
</div>
);
};
......@@ -14,6 +14,8 @@
* limitations under the License.
*/
import faker from 'faker';
import {
defaultSort,
sortBySplicedLengthDesc,
......@@ -105,21 +107,59 @@ const createTranscriptWithSmallestExons = () => {
return transcript;
};
const createMANETranscript = () => {
const transcript = createTranscript();
transcript.metadata = {
canonical: {
label: 'Ensembl canonical',
value: true,
definition: faker.lorem.sentence()
},
mane: {
label: 'MANE Select',
value: 'select',
definition: faker.lorem.sentence()
}
};
return transcript;
};
const createOtherMANETranscript = () => {
const transcript = createTranscript();
transcript.metadata = {
canonical: null,
mane: {
label: 'MANE Plus Clinical',
value: 'plus_clinical',
definition: faker.lorem.sentence()
}
};
return transcript;
};
const longProteinCodingTranscript = createLongProteinCodingTranscript();
const shortProteinCodingTranscript = createShortProteinCodingTranscript();
const longNonCodingTranscript = createLongNonCodingTranscript();
const shortNonCodingTranscript = createShortNonCodingTranscript();
const transcriptWithGreatestSplicedLength = createTranscriptWithGreatestSplicedLength();
const transcriptWithMediumSplicedLength = createTranscriptWithMediumSplicedLength();
const transcriptWithSmallestSplicedLength = createTranscriptWithSmallestSplicedLength();
const transcriptWithGreatestSplicedLength =
createTranscriptWithGreatestSplicedLength();
const transcriptWithMediumSplicedLength =
createTranscriptWithMediumSplicedLength();
const transcriptWithSmallestSplicedLength =
createTranscriptWithSmallestSplicedLength();
const transcriptWithGreatestExons = createTranscriptWithGreatestExons();
const transcriptWithMediumExons = createTranscriptWithMediumExons();
const transcriptWithSmallestExons = createTranscriptWithSmallestExons();
const maneSelectTranscript = createMANETranscript();
const otherManeTranscript = createOtherMANETranscript();
describe('default sort', () => {
it('sorts transcripts correctly', () => {
/*
- puts protein-coding transcripts first
Sorting is done in the below order
- canonical transcript
- MANE transcripts
- protein-coding transcripts
- sorts protein-coding transcripts by length
- sorts non-coding transcripts by so_term term alphabetically
*/
......@@ -128,14 +168,18 @@ describe('default sort', () => {
shortNonCodingTranscript,
shortProteinCodingTranscript,
longNonCodingTranscript, // this is the longest
longProteinCodingTranscript
otherManeTranscript,
longProteinCodingTranscript,
maneSelectTranscript
];
const expectedTranscripts = [
maneSelectTranscript,
otherManeTranscript,
longProteinCodingTranscript,
shortProteinCodingTranscript,
shortNonCodingTranscript, // its so_term is "abc"
longNonCodingTranscript // its so_term is "xyz"
longNonCodingTranscript, // its so_term is "xyz"
shortNonCodingTranscript // its so_term is "abc"
];
const sortedTranscripts = defaultSort(unsortedTranscripts);
......
......@@ -14,7 +14,6 @@
* limitations under the License.
*/
import sortBy from 'lodash/sortBy';
import partition from 'lodash/partition';
import { Pick2 } from 'ts-multipick';
......@@ -29,6 +28,7 @@ import {
import { SortingRule } from 'src/content/app/entity-viewer/state/gene-view/transcripts/geneViewTranscriptsSlice';
import { Slice } from 'src/shared/types/thoas/slice';
import { FullTranscript } from 'ensemblRoot/src/shared/types/thoas/transcript';
type SliceWithOnlyLength = Pick2<Slice, 'location', 'length'>;
......@@ -50,28 +50,45 @@ function compareTranscriptLengths(
return 0;
}
const isCanonical = (
transcript: Pick2<FullTranscript, 'metadata', 'canonical'>
) => transcript.metadata.canonical;
const isManeTranscript = (
transcript: Pick2<FullTranscript, 'metadata', 'canonical' | 'mane'>
) => transcript.metadata.mane;
export function defaultSort<
T extends Array<
IsProteinCodingTranscriptParam & {
slice: SliceWithOnlyLength;
so_term: string;
}
} & Pick2<FullTranscript, 'metadata', 'canonical' | 'mane'>
>
>(transcripts: T): T {
const [proteinCodingTranscripts, nonProteinCodingTranscripts] = partition(
const [ensemblCanonicalTranscript, nonCanonicalTranscripts] = partition(
transcripts,
isProteinCodingTranscript
isCanonical
);
proteinCodingTranscripts.sort(compareTranscriptLengths);
const sortedNonProteinCodingTranscripts = sortBy(
nonProteinCodingTranscripts,
['so_term']
const [maneTranscripts, otherTranscripts] = partition(
nonCanonicalTranscripts,
isManeTranscript
);
const [proteinCodingTranscripts, nonProteinCodingTranscripts] = partition(
otherTranscripts,
isProteinCodingTranscript
);
proteinCodingTranscripts.sort(compareTranscriptLengths);
nonProteinCodingTranscripts.sort(compareTranscriptLengths);
return [
...ensemblCanonicalTranscript,
...maneTranscripts,
...proteinCodingTranscripts,
...sortedNonProteinCodingTranscripts
...nonProteinCodingTranscripts
] as T;
}
......@@ -112,7 +129,7 @@ type GeneViewSortableTranscript = IsProteinCodingTranscriptParam &
so_term: string;
slice: SliceWithOnlyLength;
spliced_exons: unknown[];
};
} & Pick2<FullTranscript, 'metadata', 'canonical' | 'mane'>;
type SortingFunction<T extends GeneViewSortableTranscript> = (
transcript: T[]
......
......@@ -14,13 +14,16 @@
* limitations under the License.
*/
import { Source } from './source';
type ValueSetMetadata = {
value: string | number | boolean;
label: string;
definition: string;
};
type ManeMetadata = ValueSetMetadata;
type CanonicalMetadata = ValueSetMetadata;
export type Metadata = {
[key: string]: {
description: string;
value?: string;
source_uri?: string;
source?: Source;
};
export type TranscriptMetadata = {
mane: ManeMetadata | null;
canonical: CanonicalMetadata | null;
};
......@@ -19,6 +19,7 @@ import { SplicedExon } from './exon';
import { FullProductGeneratingContext } from './productGeneratingContext';
import { LocationWithinRegion } from './location';
import { ExternalReference } from './externalReference';
import { TranscriptMetadata } from './metadata';
export type FullTranscript = {
type: 'Transcript';
......@@ -32,4 +33,5 @@ export type FullTranscript = {
spliced_exons: SplicedExon[];
product_generating_contexts: FullProductGeneratingContext[];
external_references: ExternalReference[];
metadata: TranscriptMetadata;
};
......@@ -29,6 +29,7 @@ import { CDNA } from 'src/shared/types/thoas/cdna';
import { FullProductGeneratingContext } from 'src/shared/types/thoas/productGeneratingContext';
import { ProductType } from 'src/shared/types/thoas/product';
import { ExternalReference } from 'src/shared/types/thoas/externalReference';
import { TranscriptMetadata } from 'ensemblRoot/src/shared/types/thoas/metadata';
type ProteinCodingProductGeneratingContext = Omit<
FullProductGeneratingContext,
......@@ -71,10 +72,18 @@ export const createTranscript = (
product_generating_contexts: [
createProductGeneratingContext(transcriptSlice, exons)
],
metadata: createTranscriptMetadata(),
...fragment
};
};
const createTranscriptMetadata = (): TranscriptMetadata => {
return {
canonical: null,
mane: null
};
};
const createExternalReferences = (): ExternalReference[] => {
const numberOfExternalReferences = faker.datatype.number({ min: 1, max: 10 });
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment