Skip to content

Commit

Permalink
[entropy] Show CDS matches when hovering on nuc
Browse files Browse the repository at this point in the history
Slightly complicated by the observation that ribosomal frameshifts (e.g.
in SARS-CoV-2's ORF1ab) mean that a single nucleotide can appear twice
within a CDS.
  • Loading branch information
jameshadfield committed Aug 17, 2023
1 parent a8c89c2 commit 40f4ee4
Show file tree
Hide file tree
Showing 3 changed files with 95 additions and 11 deletions.
44 changes: 35 additions & 9 deletions src/components/entropy/entropyD3.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import Mousetrap from "mousetrap";
import { darkGrey, infoPanelStyles } from "../../globalStyles";
import { changeZoom } from "../../actions/entropy";
import { nucleotide_gene } from "../../util/globals";
import { getCdsByName, getNucCoordinatesFromAaPos, getCdsRangeLocalFromRangeGenome} from "../../util/entropy";
import { getCdsByName, getNucCoordinatesFromAaPos, getCdsRangeLocalFromRangeGenome,
nucleotideToAaPosition} from "../../util/entropy";

/* EntropyChart uses D3 for visualisation. There are 2 methods exposed to
* keep the visualisation in sync with React:
Expand Down Expand Up @@ -444,7 +445,6 @@ EntropyChart.prototype._clearSelectedBars = function _clearSelectedBars() {

EntropyChart.prototype._highlightSelectedBars = function _highlightSelectedBars() {
for (const d of this.selectedNodes) {
// TODO -- following needs updating once we reinstate CDS intersection
const id = this.aa ? `#cds-${this.selectedCds.name}-${d.codon}` : `#nt${d.x}`;
select(id).style("fill", "red");
}
Expand Down Expand Up @@ -1028,18 +1028,44 @@ EntropyChart.prototype._mainTooltipAa = function _mainTooltipAa(d) {

EntropyChart.prototype._mainTooltipNuc = function _mainTooltipAa(d) {
const _render = function _render(t) {
const nuc = d.x;
const aa = nucleotideToAaPosition(this.genomeMap, nuc);
let overlaps; // JSX to convey overlapping CDS info
if (aa) {
overlaps = (<div>
{`Overlaps with ${aa.length} CDS segment${aa.length>1?'s':''}:`}
<p/>
<table>
<tbody style={{fontWeight: 300}}>
<tr key="header">
<td style={{minWidth: '60px', paddingRight: '5px'}}>CDS</td>
<td style={{paddingRight: '5px'}}>Nt Pos (in CDS)</td>
<td style={{paddingRight: '5px'}}>AA Pos</td>
</tr>
{aa.map((match) => (
<tr key={match.cds.name + match.nucLocal}>
<td>{match.cds.name}</td>
<td>{match.nucLocal}</td>
<td>{match.aaLocal}</td>
</tr>
))}
</tbody>
</table>
</div>)
} else {
overlaps = (<div>
{t("No overlapping CDSs")}
</div>)
}

return (
<div className={"tooltip"} style={infoPanelStyles.tooltip}>
<div>
{t("Nucleotide {{nuc}}", {nuc: d.x})}
</div>
<div>
{t("Overlapping CDSs") + ":"}
</div>
<div>
{"Table here! TODO"}
{t("Nucleotide {{nuc}}", {nuc})}
</div>
<p/>
{overlaps}
<p/>
<div>
{this.showCounts ? `${t("Num mutations")}: ${d.y}` : `${t("entropy")}: ${d.y}`}
</div>
Expand Down
25 changes: 25 additions & 0 deletions src/util/entropy.js
Original file line number Diff line number Diff line change
Expand Up @@ -320,3 +320,28 @@ function valid(A, B, isAA) {
}
return A !== "N" && A !== "-" && B !== "N" && B !== "-";
}

/**
* Given a nucleotide in Genome space (e.g. from `rangeGenome`) find all CDSs
* which have a segment covering that nucleotide and return the local coordinate
* of that position (in both nuc + aa coordinates)
* Returns {cds: CDS; nucLocal: number; aaLocal: number}[]
*/
export function nucleotideToAaPosition(genomeMap, nucPos) {
const matches = [];
getCds(genomeMap).forEach((cds) => {
for (const segment of cds.segments) {
if (segment.rangeGenome[0] <= nucPos && segment.rangeGenome[1] >= nucPos) {
const delta = cds.strand==='+' ?
nucPos - segment.rangeGenome[0] :
segment.rangeGenome[1] - nucPos;
const nucLocal = segment.rangeLocal[0]+delta;
const aaLocal = Math.ceil(nucLocal/3);
matches.push({cds, nucLocal, aaLocal})
/* Don't return here - we want to check future segments as segments can
be overlapping */
}
}
})
return matches;
}
37 changes: 35 additions & 2 deletions test/annotation-parsing.test.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { genomeMap } from "../src/util/entropyCreateStateFromJsons";
import dataset from './data/test_complex-genome-annotation.json';
import { getNucCoordinatesFromAaPos, getCdsRangeLocalFromRangeGenome} from "../src/util/entropy";
import { getNucCoordinatesFromAaPos, getCdsRangeLocalFromRangeGenome,
nucleotideToAaPosition} from "../src/util/entropy";

const chromosome = genomeMap(dataset.meta.genome_annotations)[0];
const genome = genomeMap(dataset.meta.genome_annotations)
const chromosome = genome[0];

test("Chromosome coordinates", () => {
expect(chromosome.range[0]).toBe(1);
Expand Down Expand Up @@ -236,6 +238,37 @@ describe('Genome zoom bounds mapped to cds local coordinates', () => {
})


/**
* Note that order of CDSs (if multiple matches) is the order they
* appear in the JSON
*/
const nucleotideToAaPositionData = [
[23, [{cds: getCds('pos-single'), nucLocal: 1, aaLocal: 1}]],
[75, [{cds: getCds('neg-single'), nucLocal: 6, aaLocal: 2}]],
[9, [{cds: getCds('pos-wrapping'), nucLocal: 17, aaLocal: 6}]],
[92, [{cds: getCds('neg-wrapping'), nucLocal: 17, aaLocal: 6}]],
[4, [{cds: getCds('pos-wrapping'), nucLocal: 12, aaLocal: 4},
{cds: getCds('neg-wrapping'), nucLocal: 5, aaLocal: 2}]],
[93, [{cds: getCds('pos-wrapping'), nucLocal: 1, aaLocal: 1},
{cds: getCds('neg-wrapping'), nucLocal: 16, aaLocal: 6}]],
[48, [{cds: getCds('neg-multi'), nucLocal: 14, aaLocal: 5}]],
[63, [{cds: getCds('pos-multi'), nucLocal: 15, aaLocal: 5}]],
// Pos 36 appears in 2 segments of the pos-multi CDS, in different codons
[36, [{cds: getCds('pos-multi'), nucLocal: 6, aaLocal: 2},
{cds: getCds('pos-multi'), nucLocal: 7, aaLocal: 3}]],
// Pos 53 appears in 2 segments of the neg-multi CDS, both in the same codon
[53, [{cds: getCds('neg-multi'), nucLocal: 8, aaLocal: 3},
{cds: getCds('neg-multi'), nucLocal: 9, aaLocal: 3}]],
]


test("Single nucleotide positions are correctly mapped to amino acid positions", () => {
for (const [nucPos, expectedResult] of nucleotideToAaPositionData) {
expect(nucleotideToAaPosition(genome, nucPos)).toStrictEqual(expectedResult);
}
});


/** Assumes that the gene name wasn't specified in the JSON and thus
* we have one gene with one CDS
*/
Expand Down

0 comments on commit 40f4ee4

Please sign in to comment.