Skip to content

Commit

Permalink
Merge pull request #4165 from leexgh/exon-coordinate
Browse files Browse the repository at this point in the history
Add genomic coordinates to exon track tooltip
  • Loading branch information
inodb authored Feb 14, 2022
2 parents 6add5e7 + f11e78e commit bc4e1f3
Show file tree
Hide file tree
Showing 10 changed files with 201 additions and 64 deletions.
28 changes: 14 additions & 14 deletions packages/cbioportal-utils/src/exon/ExonUtils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,27 +218,27 @@ describe('ExonUtils', () => {
transcriptInfo.proteinLength
);

assert.equal(
assert.deepEqual(
formatExonLocation(exonInfo[0].start, 0),
'Nucleotide 1 of amino acid 1',
{ nucleotideLocation: 1, aminoAcidLocation: 1 },
'First exon always starts at 1st nucleotide of amino acid 1'
);

assert.equal(
assert.deepEqual(
formatExonLocation(exonInfo[1].start, 1),
'Nucleotide 2 of amino acid 5',
{ nucleotideLocation: 2, aminoAcidLocation: 5 },
'Second exon start location should be 2nd nucleotide of amino acid 5th, because exonInfo[1].start is 4.333333333333333, which is actrually the first exon end position, start position for second exon should be the next nucleotide of first exon end position'
);

assert.equal(
assert.deepEqual(
formatExonLocation(exonInfo[0].start + exonInfo[0].length),
'Nucleotide 1 of amino acid 5',
{ nucleotideLocation: 1, aminoAcidLocation: 5 },
'First exon ends at 4.333333333333333, which should be 1st nucleotide of amino acid 5th. No index needed to calculate end position.'
);

assert.equal(
assert.deepEqual(
formatExonLocation(exonInfo[2].start + exonInfo[2].length),
'Nucleotide 3 of amino acid 165',
{ nucleotideLocation: 3, aminoAcidLocation: 165 },
'Third exon ends at 165, which should be 3rd nucleotide of amino acid 165th. No index needed to calculate end position.'
);
});
Expand All @@ -251,21 +251,21 @@ describe('ExonUtils', () => {
transcriptInfo.proteinLength
);

assert.equal(
assert.deepEqual(
formatExonLength(exonInfo[0].length),
'4 amino acids and 1 nucleotide',
{ aminoAcidLength: 4, nucleotideLength: 1 },
'First exon length is 4.333333333333333, which is 4 amino acids and 1 nucleotide'
);

assert.equal(
assert.deepEqual(
formatExonLength(exonInfo[1].length),
'76 amino acids',
{ aminoAcidLength: 76 },
'Second exon length is 76, which is 76 nucleotide'
);

assert.equal(
assert.deepEqual(
formatExonLength(exonInfo[2].length),
'84 amino acids and 2 nucleotides',
{ aminoAcidLength: 84, nucleotideLength: 2 },
'Third exon length is 84.66666666666667, which is 84 amino acids and 2 nucleotide'
);
});
Expand Down
95 changes: 59 additions & 36 deletions packages/cbioportal-utils/src/exon/ExonUtils.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,28 @@
import { Exon, UntranslatedRegion } from 'genome-nexus-ts-api-client';
import { ExonDatum } from '../model/Exon';

export type ExonLocation = {
nucleotideLocation: number;
aminoAcidLocation: number;
};

export type ExonLength = {
nucleotideLength?: number;
aminoAcidLength: number;
};

export function extractExonInformation(
exons: Exon[],
utrs: UntranslatedRegion[],
proteinLength: number
): ExonDatum[] {
let totalLength = 0;
const exonLocList: { exonRank: number; length: number }[] = [];
const exonLocList: {
exonRank: number;
length: number;
startLocation: number;
endLocation: number;
}[] = [];
exons.forEach(exon => {
let utrStartSitesWithinExon = false;
for (let j = 0; j < utrs.length; j++) {
Expand All @@ -27,6 +42,8 @@ export function extractExonInformation(
exonLocList.push({
exonRank: exon.rank,
length: aaLength,
startLocation: exon.exonStart,
endLocation: exon.exonEnd,
});
totalLength += aaLength;
}
Expand All @@ -36,7 +53,12 @@ export function extractExonInformation(
// if there are no utr start sites within exon
if (!utrStartSitesWithinExon) {
const aaLength = (exon.exonEnd - exon.exonStart + 1) / 3;
exonLocList.push({ exonRank: exon.rank, length: aaLength });
exonLocList.push({
exonRank: exon.rank,
length: aaLength,
startLocation: exon.exonStart,
endLocation: exon.exonEnd,
});
totalLength += aaLength;
}
});
Expand All @@ -54,6 +76,8 @@ export function extractExonInformation(
rank: exon.exonRank,
length: exon.length,
start: startOfExon,
genomicLocationStart: exon.startLocation,
genomicLocationEnd: exon.endLocation,
};
startOfExon += exon.length;
return exonDatum;
Expand All @@ -64,64 +88,63 @@ export function extractExonInformation(
// Generate exon location description by exon location number.
// Description should follow this format: "Nucleotide xx of amino acid xx".
// Also need to make it clear which location is inclusive for start position and last end position
export function formatExonLocation(exonLocation: number, index?: number) {
export function formatExonLocation(
exonLocation: number,
index?: number
): ExonLocation {
const numNucleotidesOver = Math.round(exonLocation * 3) % 3;
// first exon starts at 1st nucleotide of amino acid 1
if (index === 0) {
return 'Nucleotide 1 of amino acid 1';
return { nucleotideLocation: 1, aminoAcidLocation: 1 };
} else if (index !== 0 && index !== undefined) {
// exon start location should be next nucleotide from previous end location
// we should use floor() to get integer part 'x' from 'x.zzzzzzz'(e.g. 4.333333), use round() will get 'x+1' sometimes
if (numNucleotidesOver === 0) {
return (
'Nucleotide 1 of amino acid ' +
(Math.floor(exonLocation) + 1).toString()
);
return {
nucleotideLocation: 1,
aminoAcidLocation: Math.floor(exonLocation) + 1,
};
} else if (numNucleotidesOver === 1) {
return (
'Nucleotide 2 of amino acid ' +
(Math.floor(exonLocation) + 1).toString()
);
return {
nucleotideLocation: 2,
aminoAcidLocation: Math.floor(exonLocation) + 1,
};
} else {
return (
'Nucleotide 3 of amino acid ' +
(Math.floor(exonLocation) + 1).toString()
);
return {
nucleotideLocation: 3,
aminoAcidLocation: Math.floor(exonLocation) + 1,
};
}
} else {
// exon end location
if (numNucleotidesOver === 0) {
return (
'Nucleotide 3 of amino acid ' +
Math.floor(exonLocation).toString()
);
return {
nucleotideLocation: 3,
aminoAcidLocation: Math.floor(exonLocation),
};
} else if (numNucleotidesOver === 1) {
return (
'Nucleotide 1 of amino acid ' +
(Math.floor(exonLocation) + 1).toString()
);
return {
nucleotideLocation: 1,
aminoAcidLocation: Math.floor(exonLocation) + 1,
};
} else {
return (
'Nucleotide 2 of amino acid ' +
(Math.floor(exonLocation) + 1).toString()
);
return {
nucleotideLocation: 2,
aminoAcidLocation: Math.floor(exonLocation) + 1,
};
}
}
}

// Generate exon length description by exon length
// Description should follow this format: "xx amino acids and xx nucleotides".
export function formatExonLength(exonLength: number) {
export function formatExonLength(exonLength: number): ExonLength {
const numNucleotidesOver = Math.round(exonLength * 3) % 3;
if (numNucleotidesOver === 0) {
return Math.floor(exonLength).toString() + ' amino acids';
return { aminoAcidLength: Math.floor(exonLength) };
} else if (numNucleotidesOver === 1) {
return (
Math.floor(exonLength).toString() + ' amino acids and 1 nucleotide'
);
return { aminoAcidLength: Math.floor(exonLength), nucleotideLength: 1 };
} else {
return (
Math.floor(exonLength).toString() + ' amino acids and 2 nucleotides'
);
return { aminoAcidLength: Math.floor(exonLength), nucleotideLength: 2 };
}
}
8 changes: 7 additions & 1 deletion packages/cbioportal-utils/src/model/Exon.ts
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
export type ExonDatum = { rank: number; length: number; start: number };
export type ExonDatum = {
rank: number;
length: number;
start: number;
genomicLocationStart: number;
genomicLocationEnd: number;
};
73 changes: 64 additions & 9 deletions packages/react-mutation-mapper/src/component/track/ExonTrack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,15 @@ export default class ExonTrack extends React.Component<ExonTrackProps, {}> {
: undefined;
}

@computed get chromosome(): string {
return this.props.store.ensemblTranscriptLookUp.result?.body
?.seq_region_name;
}

@computed get genomeBuild(): string {
return this.props.store.genomeBuild;
}

@computed get exonSpecs(): TrackItemSpec[] {
if (!this.transcriptId || !this.transcript) {
return [];
Expand All @@ -52,12 +61,18 @@ export default class ExonTrack extends React.Component<ExonTrackProps, {}> {
? exonInfo.map((exon: ExonDatum, index: number) => {
const startCodon = exon.start;
const endCodon = exon.start + exon.length;
const exonLength = exon.length;
const isSkippable = Number.isInteger(exonLength);
const stringStart = formatExonLocation(startCodon, index);
const stringEnd = formatExonLocation(endCodon);
const stringLength = formatExonLength(exonLength);

const exonStartLocation = formatExonLocation(
startCodon,
index
);
const exonEndLocation = formatExonLocation(endCodon);
const exonLength = formatExonLength(exon.length);
const link = this.chromosome
? `https://igv.org/app/?locus=chr${this.chromosome}:${exon.genomicLocationStart}-${exon.genomicLocationEnd}&genome=${this.genomeBuild}`
: 'https://igv.org';
const linkText = this.chromosome
? `${this.genomeBuild}:chr${this.chromosome}:${exon.genomicLocationStart} - ${exon.genomicLocationEnd}`
: `${exon.genomicLocationStart} - ${exon.genomicLocationEnd}`;
return {
color: altColors[index % 2],
startCodon: startCodon,
Expand All @@ -67,11 +82,51 @@ export default class ExonTrack extends React.Component<ExonTrackProps, {}> {
tooltip: (
<span>
<h5> Exon {exon.rank} </h5>
Start: {stringStart}
Start: Nucleotide{' '}
<strong>
{exonStartLocation.nucleotideLocation}
</strong>{' '}
of amino acid{' '}
<strong>
{exonStartLocation.aminoAcidLocation}
</strong>
<br></br>
End: Nucleotide{' '}
<strong>
{exonEndLocation.nucleotideLocation}
</strong>{' '}
of amino acid{' '}
<strong>
{exonEndLocation.aminoAcidLocation}
</strong>
<br></br>
End: {stringEnd}
Length:{' '}
<strong>{exonLength.aminoAcidLength}</strong>{' '}
amino acids{' '}
{exonLength.nucleotideLength && (
<>
{' '}
and{' '}
<strong>
{exonLength.nucleotideLength}
</strong>{' '}
nucleotides
</>
)}
<br></br>
Length: {stringLength}
Genomic location:
{` `}
<a
target="_blank"
href={link}
rel="noopener noreferrer"
>
<>
{linkText}
{` `}
<i className="fa fa-external-link" />
</>
</a>
</span>
),
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ export interface MutationMapperStore<T extends Mutation> {
transcriptsWithAnnotations: RemoteData<string[] | undefined>;
transcriptsWithProteinLength: RemoteData<string[] | undefined>;
mutationsByTranscriptId: { [transcriptId: string]: T[] };
ensemblTranscriptLookUp: RemoteData<any | Error | undefined>;
genomeBuild: string;
setSelectedTranscript?: (id: string | undefined) => void;
getTranscriptId?: () => string | undefined;
selectedTranscript?: string | undefined;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ import {
import { DefaultMutationMapperDataStore } from './DefaultMutationMapperDataStore';
import { DefaultMutationMapperDataFetcher } from './DefaultMutationMapperDataFetcher';
import { DefaultMutationMapperFilterApplier } from './DefaultMutationMapperFilterApplier';
import { get } from 'superagent';

interface DefaultMutationMapperStoreConfig {
annotationFields?: string[];
Expand All @@ -95,6 +96,7 @@ interface DefaultMutationMapperStoreConfig {
selectionFilters?: DataFilter[];
highlightFilters?: DataFilter[];
groupFilters?: { group: string; filter: DataFilter }[];
genomeBuild?: string;
}

class DefaultMutationMapperStore<T extends Mutation>
Expand Down Expand Up @@ -1115,6 +1117,30 @@ class DefaultMutationMapperStore<T extends Mutation>
0
);
}

readonly ensemblTranscriptLookUp: MobxPromise<any | undefined> = remoteData(
{
await: () => [this.activeTranscript],
invoke: async () => {
const ensemblTranscriptLookUpLink =
this.genomeBuild === 'hg19'
? `https://grch37.rest.ensembl.org/lookup/id/${this.activeTranscript.result}?content-type=application/json`
: `https://rest.ensembl.org/lookup/id/${this.activeTranscript.result}?content-type=application/json`;
return this.activeTranscript.result
? get(ensemblTranscriptLookUpLink)
: undefined;
},
onError: () => {
// fail silently, leave the error handling responsibility to the data consumer
},
}
);

@computed
// Get genome build for exon track
public get genomeBuild(): string {
return this.config.genomeBuild || 'hg19';
}
}

export default DefaultMutationMapperStore;
Loading

0 comments on commit bc4e1f3

Please sign in to comment.