Skip to content

Commit 6407792

Browse files
Merge pull request #1657 from nextstrain/feat/labeled-aa-muts
2 parents 0ea1eed + 9216056 commit 6407792

File tree

12 files changed

+483
-90
lines changed

12 files changed

+483
-90
lines changed

packages/nextclade-web/src/components/Results/ColumnMutations.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { getSafeId } from 'src/helpers/getSafeId'
99
import { TableSlim } from 'src/components/Common/TableSlim'
1010
import { Tooltip } from 'src/components/Results/Tooltip'
1111
import { ListOfNucMuts } from 'src/components/Results/ListOfNucMuts'
12-
import { ListOfAaSubs } from 'src/components/SequenceView/ListOfAaSubs'
12+
import { ListOfAaMuts } from 'src/components/Results/ListOfAaMuts'
1313
import { useTranslationSafe } from 'src/helpers/useTranslationSafe'
1414

1515
export function ColumnMutations({ analysisResult }: ColumnCladeProps) {
@@ -100,7 +100,7 @@ export function ColumnMutations({ analysisResult }: ColumnCladeProps) {
100100
</tr>
101101
<tr>
102102
<td>
103-
<ListOfAaSubs analysisResult={analysisResult} />
103+
<ListOfAaMuts analysisResult={analysisResult} />
104104
</td>
105105
</tr>
106106
</tbody>
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import React from 'react'
2+
3+
import type { AaSub } from 'src/types'
4+
import { AminoacidMutationBadge } from 'src/components/Common/MutationBadge'
5+
import { useTranslationSafe } from 'src/helpers/useTranslationSafe'
6+
import { splitToRows } from 'src/components/Results/splitToRows'
7+
import { TableSlim } from 'src/components/Common/TableSlim'
8+
9+
export interface ListOfAaMutationsGenericProps {
10+
substitutions: AaSub[]
11+
}
12+
13+
export function ListOfAaMutationsGeneric({ substitutions }: ListOfAaMutationsGenericProps) {
14+
const { t } = useTranslationSafe()
15+
16+
const totalMutations = substitutions.length
17+
const maxRows = Math.min(8, totalMutations)
18+
const numCols = 8
19+
const substitutionsSelected = substitutions.slice(0, maxRows * numCols)
20+
const columns = splitToRows(substitutionsSelected, { rowLength: maxRows })
21+
22+
let moreText
23+
if (totalMutations > substitutionsSelected.length) {
24+
moreText = t('(truncated)')
25+
}
26+
27+
return (
28+
<div className="d-flex">
29+
<div className="mr-auto">
30+
<TableSlim>
31+
<tbody>
32+
{columns.map((col, i) => (
33+
// eslint-disable-next-line react/no-array-index-key
34+
<tr key={i}>
35+
{col.map((item) => (
36+
<td key={item.pos}>{<AminoacidMutationBadge mutation={item} />}</td>
37+
))}
38+
</tr>
39+
))}
40+
41+
{moreText && (
42+
<tr>
43+
<td colSpan={numCols} className="text-center">
44+
{moreText}
45+
</td>
46+
</tr>
47+
)}
48+
</tbody>
49+
</TableSlim>
50+
</div>
51+
</div>
52+
)
53+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import React, { useMemo } from 'react'
2+
3+
import { uniq } from 'lodash'
4+
import styled from 'styled-components'
5+
6+
import type { AaSubLabeled } from 'src/types'
7+
import { TableSlim } from 'src/components/Common/TableSlim'
8+
import { AminoacidMutationBadge } from 'src/components/Common/MutationBadge'
9+
import { useTranslationSafe } from 'src/helpers/useTranslationSafe'
10+
11+
const MutationLabelBadge = styled.span`
12+
background-color: ${(props) => props.theme.gray300};
13+
border-radius: 3px;
14+
box-shadow: ${(props) => props.theme.shadows.slight};
15+
margin: 3px 5px;
16+
padding: 0 3px;
17+
font-family: ${(props) => props.theme.font.monospace};
18+
`
19+
20+
export interface MutationLabelProps {
21+
label: string
22+
}
23+
24+
export function MutationLabelBadgeComponent({ label }: MutationLabelProps) {
25+
return <MutationLabelBadge>{label}</MutationLabelBadge>
26+
}
27+
28+
export interface ListOfAaMutationsLabeledProps {
29+
mutationsLabeled: AaSubLabeled[]
30+
}
31+
32+
export function ListOfAaMutationsLabeled({ mutationsLabeled }: ListOfAaMutationsLabeledProps) {
33+
const { t } = useTranslationSafe()
34+
35+
const labeledMutationRows = useMemo(
36+
() =>
37+
mutationsLabeled.map(({ substitution, labels }) => {
38+
let labelsTruncated = uniq(labels)
39+
let labelsTruncatedStr = ''
40+
if (labels.length > 6) {
41+
labelsTruncated = labels.slice(0, 7)
42+
labelsTruncatedStr = ', ...more'
43+
}
44+
45+
const labelComponents = labelsTruncated.map((label) => (
46+
<MutationLabelBadgeComponent key={label} label={label} />
47+
))
48+
49+
return (
50+
<tr key={substitution.pos}>
51+
<td>
52+
<AminoacidMutationBadge mutation={substitution} />
53+
</td>
54+
<td>
55+
<span>{labelComponents}</span>
56+
<span>{labelsTruncatedStr}</span>
57+
</td>
58+
</tr>
59+
)
60+
}),
61+
[mutationsLabeled],
62+
)
63+
64+
return (
65+
<div className="d-flex">
66+
<div className="mr-auto">
67+
<TableSlim>
68+
<thead>
69+
<tr>
70+
<th>{t('Mutation')}</th>
71+
<th>{t('Labels')}</th>
72+
</tr>
73+
</thead>
74+
<tbody>{labeledMutationRows}</tbody>
75+
</TableSlim>
76+
</div>
77+
</div>
78+
)
79+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { isEmpty, isNil } from 'lodash'
2+
import React, { ReactNode } from 'react'
3+
import { useRecoilValue } from 'recoil'
4+
import { REF_NODE_PARENT, REF_NODE_ROOT } from 'src/constants'
5+
import { getAaMutations } from 'src/helpers/relativeMuts'
6+
import { viewedDatasetNameAtom } from 'src/state/dataset.state'
7+
import { currentRefNodeNameAtom } from 'src/state/results.state'
8+
import { type AnalysisResult, AaSub, AaSubLabeled } from 'src/types'
9+
import { LiInvisible, UlInvisible } from 'src/components/Common/List'
10+
import { useTranslationSafe } from 'src/helpers/useTranslationSafe'
11+
import { TableSlim } from 'src/components/Common/TableSlim'
12+
import { ListOfAaMutationsGeneric } from './ListOfAaMutationsGeneric'
13+
import { ListOfAaMutationsLabeled } from './ListOfAaMutationsLabeled'
14+
15+
export interface ListOfPrivateAaMutationsProps {
16+
analysisResult: AnalysisResult
17+
}
18+
19+
export function ListOfAaMuts({ analysisResult }: ListOfPrivateAaMutationsProps) {
20+
const { t } = useTranslationSafe()
21+
22+
const datasetName = useRecoilValue(viewedDatasetNameAtom)
23+
const refNodeName = useRecoilValue(currentRefNodeNameAtom({ datasetName }))
24+
const muts = getAaMutations(analysisResult, refNodeName ?? REF_NODE_ROOT)
25+
if (!muts) {
26+
return null
27+
}
28+
29+
const { aaSubs, relAaMuts } = muts
30+
31+
// Flatten all the private mutations from all genes
32+
const allReversionSubstitutions = relAaMuts?.flatMap((m) => m.reversionSubstitutions) ?? []
33+
const allLabeledSubstitutions = relAaMuts?.flatMap((m) => m.labeledSubstitutions) ?? []
34+
35+
return (
36+
<div className="d-flex">
37+
<div className="mr-auto">
38+
<UlInvisible className="pl-2">
39+
<ListOfAaMutationsOneType heading={t('All substitutions ({{ n }})', { n: aaSubs.length })} subs={aaSubs} />
40+
</UlInvisible>
41+
42+
<UlInvisible className="pl-2">
43+
<ListOfAaMutationsOneType
44+
heading={t('Reversion substitutions ({{ n }})', { n: allReversionSubstitutions.length })}
45+
subs={allReversionSubstitutions}
46+
/>
47+
48+
{refNodeName === REF_NODE_PARENT && (
49+
<ListOfAaMutationsOneTypeLabelled
50+
heading={t('Labeled substitutions ({{ n }})', { n: allLabeledSubstitutions.length })}
51+
subs={allLabeledSubstitutions}
52+
/>
53+
)}
54+
55+
{refNodeName !== REF_NODE_PARENT && (
56+
<small>
57+
{t(
58+
'To see private mutations (and labeled mutations if applicable), switch to "Parent" in the "Relative to" dropdown',
59+
)}
60+
</small>
61+
)}
62+
</UlInvisible>
63+
</div>
64+
</div>
65+
)
66+
}
67+
68+
function ListOfAaMutationsOneType({ heading, subs }: { heading: ReactNode; subs: AaSub[] | undefined }) {
69+
if (isNil(subs) || isEmpty(subs)) {
70+
return null
71+
}
72+
73+
return (
74+
<LiInvisible>
75+
<TableSlim className="mb-0">
76+
<tbody>
77+
<tr>
78+
<td>{heading}</td>
79+
</tr>
80+
<tr>
81+
<td>
82+
<ListOfAaMutationsGeneric substitutions={subs} />
83+
</td>
84+
</tr>
85+
</tbody>
86+
</TableSlim>
87+
</LiInvisible>
88+
)
89+
}
90+
91+
function ListOfAaMutationsOneTypeLabelled({ heading, subs }: { heading: ReactNode; subs: AaSubLabeled[] | undefined }) {
92+
if (isNil(subs) || isEmpty(subs)) {
93+
return null
94+
}
95+
96+
return (
97+
<LiInvisible>
98+
<TableSlim className="mb-0">
99+
<tbody>
100+
<tr>
101+
<td>{heading}</td>
102+
</tr>
103+
<tr>
104+
<td>
105+
<ListOfAaMutationsLabeled mutationsLabeled={subs} />
106+
</td>
107+
</tr>
108+
</tbody>
109+
</TableSlim>
110+
</LiInvisible>
111+
)
112+
}

packages/nextclade-web/src/components/Results/ListOfNucMuts.tsx

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,6 @@ export function ListOfNucMuts({ analysisResult }: ListOfPrivateNucMutationsProps
4747
subs={relMuts?.labeledSubstitutions}
4848
/>
4949
)}
50-
51-
{refNodeName === REF_NODE_PARENT && (
52-
<ListOfNucMutationsOneType
53-
heading={t('Unlabeled substitutions ({{ n }})', { n: relMuts?.unlabeledSubstitutions.length })}
54-
subs={relMuts?.unlabeledSubstitutions}
55-
/>
56-
)}
5750
</UlInvisible>
5851
</div>
5952
</div>

packages/nextclade-web/src/components/SequenceView/ListOfAaSubs.tsx

Lines changed: 0 additions & 78 deletions
This file was deleted.

0 commit comments

Comments
 (0)