Skip to content

Commit

Permalink
feat(web): add sequence markers for ambiguous nucleotides
Browse files Browse the repository at this point in the history
Resolves: #700

Add colored markers for ambiguous nucleotide to nucleotide sequence view.

I tried to pick colors to be pale (so that they resemble pale grey of missing nucs) and to be a mixture of colors of corresponding nucs, or to be inverse color in case of "not" characters (e.g. color of not-A is a pale inverse of color of A).

I made them half-height at the top by default, just like missing nucs. (Settings UI is currently broken, so it's impossible to change at the moment, sorry)

Tooltip text is as specified in the original issue.
  • Loading branch information
ivan-aksamentov committed Oct 18, 2023
1 parent 325acb4 commit afae45c
Show file tree
Hide file tree
Showing 5 changed files with 164 additions and 13 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import React, { SVGProps, useCallback, useMemo, useState } from 'react'
import { useRecoilValue } from 'recoil'
import { get } from 'lodash'
import { useTranslationSafe as useTranslation } from 'src/helpers/useTranslationSafe'
import type { NucleotideRange } from 'src/types'
import { Tooltip } from 'src/components/Results/Tooltip'
import { BASE_MIN_WIDTH_PX } from 'src/constants'
import { formatRange } from 'src/helpers/formatRange'
import { getNucleotideColor } from 'src/helpers/getNucleotideColor'
import { getSafeId } from 'src/helpers/getSafeId'
import {
getSeqMarkerDims,
SeqMarkerHeightState,
seqMarkerAmbiguousHeightStateAtom,
} from 'src/state/seqViewSettings.state'

export interface AmbiguousViewProps extends SVGProps<SVGRectElement> {
index: number
seqName: string
ambiguous: NucleotideRange
pixelsPerBase: number
}

export function SequenceMarkerAmbiguousUnmemoed({
index,
seqName,
ambiguous,
pixelsPerBase,
...rest
}: AmbiguousViewProps) {
const { t } = useTranslation()
const [showTooltip, setShowTooltip] = useState(false)
const onMouseEnter = useCallback(() => setShowTooltip(true), [])
const onMouseLeave = useCallback(() => setShowTooltip(false), [])

const seqMarkerAmbiguousHeightState = useRecoilValue(seqMarkerAmbiguousHeightStateAtom)
const { y, height } = useMemo(() => getSeqMarkerDims(seqMarkerAmbiguousHeightState), [seqMarkerAmbiguousHeightState])

const { character, range: { begin, end }, range } = ambiguous // prettier-ignore
const rangeStr = formatRange(range)

const text = useMemo(() => {
const AMBIGUOUS_NUCS = {
R: t('A or G'),
K: t('G or T'),
S: t('G or C'),
Y: t('C or T'),
M: t('A or C'),
W: t('A or T'),
B: t('not A (C, G or T)'),
H: t('not G (A, C or T)'),
D: t('not C (A, G or T)'),
V: t('not T (A, C or G)'),
}
const unaliased = get(AMBIGUOUS_NUCS, character) ?? ''

let text = `${rangeStr}: ${character}`
if (unaliased) {
text = `${text} (${unaliased})`
}

return text
}, [character, rangeStr, t])

const ambiguousColor = getNucleotideColor(character)

const id = getSafeId('ambiguous-marker', { index, seqName, character, begin, end })
let width = (end - begin) * pixelsPerBase
width = Math.max(width, BASE_MIN_WIDTH_PX)
const halfNuc = Math.max(pixelsPerBase, BASE_MIN_WIDTH_PX) / 2 // Anchor in the center of the first nuc
const x = begin * pixelsPerBase - halfNuc

if (seqMarkerAmbiguousHeightState === SeqMarkerHeightState.Off) {
return null
}

return (
<rect
id={id}
fill={ambiguousColor}
x={x}
y={y}
width={width}
height={height}
{...rest}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
<Tooltip target={id} isOpen={showTooltip}>
<p className="m-0">{t('Ambiguous:')}</p>
<p className="m-0">{text}</p>
</Tooltip>
</rect>
)
}

export const SequenceMarkerAmbiguous = React.memo(SequenceMarkerAmbiguousUnmemoed)
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react'
import { ReactResizeDetectorDimensions, withResizeDetector } from 'react-resize-detector'
import { useRecoilValue } from 'recoil'
import { SequenceMarkerAmbiguous } from 'src/components/SequenceView/SequenceMarkerAmbiguous'
import { useTranslationSafe } from 'src/helpers/useTranslationSafe'
import { maxNucMarkersAtom } from 'src/state/seqViewSettings.state'
import styled from 'styled-components'
Expand Down Expand Up @@ -39,8 +40,18 @@ export interface SequenceViewProps extends ReactResizeDetectorDimensions {
}

export function SequenceViewUnsized({ sequence, width }: SequenceViewProps) {
const { index, seqName, substitutions, missing, deletions, alignmentRange, frameShifts, insertions, nucToAaMuts } =
sequence
const {
index,
seqName,
substitutions,
missing,
deletions,
alignmentRange,
frameShifts,
insertions,
nucToAaMuts,
nonACGTNs,
} = sequence

const { t } = useTranslationSafe()
const maxNucMarkers = useRecoilValue(maxNucMarkersAtom)
Expand Down Expand Up @@ -82,6 +93,18 @@ export function SequenceViewUnsized({ sequence, width }: SequenceViewProps) {
)
})

const ambigViews = nonACGTNs.map((ambig) => {
return (
<SequenceMarkerAmbiguous
key={ambig.range.begin}
index={index}
seqName={seqName}
ambiguous={ambig}
pixelsPerBase={pixelsPerBase}
/>
)
})

const deletionViews = deletions.map((deletion) => {
return (
<SequenceMarkerGap
Expand Down Expand Up @@ -148,6 +171,7 @@ export function SequenceViewUnsized({ sequence, width }: SequenceViewProps) {
/>
{mutationViews}
{missingViews}
{ambigViews}
{deletionViews}
{insertionViews}
<SequenceMarkerUnsequencedEnd
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
seqMarkerMutationHeightStateAtom,
seqMarkerUnsequencedHeightStateAtom,
maxNucMarkersAtom,
seqMarkerAmbiguousHeightStateAtom,
} from 'src/state/seqViewSettings.state'

/** Adapts Recoil state `enum` to `string` */
Expand Down Expand Up @@ -53,6 +54,10 @@ export function SeqViewSettings() {
seqMarkerMissingHeightStateAtom,
)

const [seqMarkerAmbiguousHeightState, setSeqMarkerAmbiguousHeightState] = useSeqMarkerState(
seqMarkerAmbiguousHeightStateAtom,
)

const [seqMarkerGapHeightState, setSeqMarkerGapHeightState] = useSeqMarkerState(seqMarkerGapHeightStateAtom)

const [seqMarkerMutationHeightState, setSeqMarkerMutationHeightState] = useSeqMarkerState(
Expand Down Expand Up @@ -89,6 +94,15 @@ export function SeqViewSettings() {
/>
</FormGroup>

<FormGroup>
{t('Ambiguous')}
<Multitoggle
values={SEQ_MARKER_HEIGHT_STATES}
value={seqMarkerAmbiguousHeightState}
onChange={setSeqMarkerAmbiguousHeightState}
/>
</FormGroup>

<FormGroup>
<Label>
{t('Gaps')}
Expand Down
16 changes: 13 additions & 3 deletions packages_rs/nextclade-web/src/helpers/getNucleotideColor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,21 @@ import { get } from 'lodash'
import { Nucleotide } from 'src/types'

export const NUCLEOTIDE_COLORS: Record<string, string> = {
'A': '#B54330',
'C': '#3C5BD6',
'G': '#9C8D1C',
'A': '#b54330',
'C': '#3c5bd6',
'G': '#9c8d1c',
'T': '#409543',
'N': '#555555',
'R': '#bd8262',
'K': '#92a364',
'S': '#61a178',
'Y': '#5e959e',
'M': '#897198',
'W': '#a0a665',
'B': '#5b9fbd',
'H': '#949ce1',
'D': '#d8cda0',
'V': '#b496b3',
'-': '#777777',
} as const

Expand Down
22 changes: 14 additions & 8 deletions packages_rs/nextclade-web/src/state/seqViewSettings.state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,14 @@ export function seqMarkerHeightStateToString(val: SeqMarkerHeightState) {
export function seqMarkerHeightStateFromString(key: string) {
// prettier-ignore
switch (key) {
case "Top":
return SeqMarkerHeightState.Top;
case "Bottom":
return SeqMarkerHeightState.Bottom;
case "Full":
return SeqMarkerHeightState.Full;
case "Off":
return SeqMarkerHeightState.Off;
case 'Top':
return SeqMarkerHeightState.Top
case 'Bottom':
return SeqMarkerHeightState.Bottom
case 'Full':
return SeqMarkerHeightState.Full
case 'Off':
return SeqMarkerHeightState.Off
}
throw new ErrorInternal(`When converting string to 'SeqMarkerHeightState': Unknown variant'${key}'`)
}
Expand All @@ -52,6 +52,12 @@ export const seqMarkerMissingHeightStateAtom = atom<SeqMarkerHeightState>({
effects: [persistAtom],
})

export const seqMarkerAmbiguousHeightStateAtom = atom<SeqMarkerHeightState>({
key: 'seqMarkerAmbiguousHeightStateAtom',
default: SeqMarkerHeightState.Top,
effects: [persistAtom],
})

export const seqMarkerGapHeightStateAtom = atom<SeqMarkerHeightState>({
key: 'seqMarkerGapHeight',
default: SeqMarkerHeightState.Full,
Expand Down

0 comments on commit afae45c

Please sign in to comment.