-
Notifications
You must be signed in to change notification settings - Fork 163
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Categorise mutations in tip-clicked panel
These changes were motivated by issue #1444 [1] where separating mutations into categories can aid both QC and biological interpretation. I chose to use "mutations" to refer to mutations observed on a branch and "changes" to refer to the collection of mutations between a tip and the root. The categories are not necessarily disjoint, as a mutation back to the root will also be a homoplasy or a unique mutation. Note that changes between a tip sequence and the root aren't grouped into homoplasies, as a single change (A→C) may be the result of multiple mutations (e.g. A→B→C) and thus we would need to check the tip state of each position which is difficult with the current code. On-hover panels are left unchanged in this commit. [1] #1444
- Loading branch information
1 parent
68f57f6
commit bec7d82
Showing
5 changed files
with
269 additions
and
100 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
import React from "react"; | ||
import styled from 'styled-components'; | ||
import { getBranchMutations, getTipChanges } from "../../../util/treeMiscHelpers"; | ||
|
||
|
||
const Button = styled.button` | ||
border: 0px; | ||
background-color: inherit; | ||
cursor: pointer; | ||
outline: 0; | ||
text-decoration: underline; | ||
font-size: 16px; | ||
padding: 0px 0px; | ||
`; | ||
|
||
const mutSortFn = (a, b) => { | ||
const [aa, bb] = [parseInt(a.slice(1, -1), 10), parseInt(b.slice(1, -1), 10)]; | ||
return aa<bb ? -1 : 1; | ||
}; | ||
|
||
const Heading = styled.p` | ||
margin-top: 12px; | ||
margin-bottom: 4px; | ||
`; | ||
const SubHeading = styled.span` | ||
padding-right: 8px; | ||
`; | ||
const MutationList = styled.span` | ||
`; | ||
const MutationLine = styled.p` | ||
margin: 0px 0px 4px 0px; | ||
font-weight: 300; | ||
font-size: 16px; | ||
`; | ||
const TableFirstColumn = styled.td` | ||
font-weight: 500; | ||
white-space: nowrap; | ||
vertical-align: baseline; | ||
`; | ||
const ListOfMutations = ({title, muts}) => ( | ||
<MutationLine> | ||
<SubHeading key={title}>{title}</SubHeading> | ||
<MutationList>{muts.sort(mutSortFn).join(", ")}</MutationList> | ||
</MutationLine> | ||
); | ||
|
||
const mutCategoryLookup = { | ||
unique: "Unique", | ||
changes: "Changes", | ||
homoplasies: "Homoplasies", | ||
reversionsToRoot: "Reversions to root", | ||
gaps: "Gaps", | ||
ns: "Ns " | ||
}; | ||
|
||
/** | ||
* Returns a TSV-style string of all mutations / changes | ||
*/ | ||
const mutationsToTsv = (categorisedMutations, geneSortFn) => | ||
Object.keys(categorisedMutations).sort(geneSortFn).map((gene) => | ||
Object.keys(mutCategoryLookup) | ||
.filter((key) => (key in categorisedMutations[gene] && categorisedMutations[gene][key].length)) | ||
.map((key) => | ||
`${gene}\t${key}\t${categorisedMutations[gene][key].sort(mutSortFn).join(", ")}` | ||
) | ||
).flat() | ||
.join("\n"); | ||
|
||
/** | ||
* Returns a table row element for the (categorised) mutations for the given gene | ||
* @returns {(ReactComponent|null)} | ||
*/ | ||
const displayGeneMutations = (gene, mutsPerCat) => { | ||
/* check if any categories have entries for us to display */ | ||
if (Object.values(mutsPerCat).filter((lst) => lst.length).length === 0) { | ||
return null; | ||
} | ||
return ( | ||
<tr key={gene}> | ||
<TableFirstColumn>{gene==="nuc" ? "Nt" : gene}</TableFirstColumn> | ||
<td> | ||
{Object.entries(mutCategoryLookup).map(([key, name]) => ( | ||
(key in mutsPerCat && mutsPerCat[key].length) ? | ||
(<ListOfMutations | ||
title={`${name} (${mutsPerCat[key].length}):`} | ||
muts={mutsPerCat[key]} | ||
/>) : | ||
null | ||
))} | ||
</td> | ||
</tr> | ||
); | ||
}; | ||
|
||
export const MutationTable = ({node, geneSortFn, isTip, observedMutations}) => { | ||
const categorisedMutations = isTip ? | ||
getTipChanges(node) : | ||
getBranchMutations(node, observedMutations); | ||
|
||
if (Object.keys(categorisedMutations).length===0) { | ||
return ( | ||
<Heading> | ||
{isTip ? `No sequence changes observed` : `No mutations observed on branch`} | ||
</Heading> | ||
); | ||
} | ||
|
||
return ( | ||
<> | ||
<Heading> | ||
{isTip ? `Sequence changes observed (from root):` : `Mutations observed on branch:`} | ||
</Heading> | ||
<table> | ||
{Object.keys(categorisedMutations).sort(geneSortFn).map( | ||
(gene) => displayGeneMutations(gene, categorisedMutations[gene], isTip) | ||
)} | ||
<tr> | ||
<td/> | ||
<td> | ||
<Button onClick={() => {navigator.clipboard.writeText(mutationsToTsv(categorisedMutations, geneSortFn));}}> | ||
{`Click to copy all ${isTip ? 'changes' : 'mutations'} to clipboard as TSV`} | ||
</Button> | ||
</td> | ||
</tr> | ||
</table> | ||
</> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.