Skip to content

Commit

Permalink
feat: group clades in mutation tooltips on gene map
Browse files Browse the repository at this point in the history
  • Loading branch information
ivan-aksamentov committed Jun 29, 2020
1 parent e5df009 commit 6971f9c
Show file tree
Hide file tree
Showing 5 changed files with 111 additions and 75 deletions.
40 changes: 40 additions & 0 deletions packages/web/src/algorithms/clades.ts
@@ -0,0 +1,40 @@
import { groupBy, uniqBy } from 'lodash'

import type { Nucleotide, Substitutions, CladeDataFlat, CladeDataGrouped } from 'src/algorithms/types'

import { VIRUSES } from 'src/algorithms/viruses'

export function cladesFlatten(clades: Substitutions): CladeDataFlat[] {
return Object.entries(clades).reduce((result, [cladeName, substitutions]) => {
const subs = substitutions.map(({ pos, nuc }) => {
return { cladeName, pos, nuc }
})

return [...result, ...subs]
}, [] as CladeDataFlat[])
}

export function groupClades(clades: Substitutions): CladeDataGrouped[] {
const cladesFlat = cladesFlatten(clades)

// TODO: there should probably be a simpler and cleaner way to do this
const cladesGroupedByNuc = cladesFlat.map(({ pos }) => {
const subsList = cladesFlat
.filter((candidate) => pos === candidate.pos)
.map(({ cladeName, nuc }) => ({ cladeName, nuc }))

const subsRaw = groupBy(subsList, ({ nuc }) => nuc)

const subsEntriesSimplified = Object.entries(subsRaw).map(([nuc, arr]) => [
nuc,
arr.map(({ cladeName }) => cladeName),
])

const subs = Object.fromEntries(subsEntriesSimplified) as Record<Nucleotide, string[]>
return { pos, subs }
})

return uniqBy(cladesGroupedByNuc, ({ pos }) => pos)
}

export const cladesGrouped = groupClades(VIRUSES['SARS-CoV-2'].clades)
4 changes: 2 additions & 2 deletions packages/web/src/algorithms/run.ts
@@ -1,7 +1,7 @@
import { pickBy } from 'lodash'
import { pickBy, groupBy } from 'lodash'
import { VIRUSES } from './viruses'

import type { AnalysisParams, AnalysisResult } from './types'
import type { AnalysisParams, AnalysisResult, Substitutions } from './types'
import { geneMap } from './geneMap'
import { parseSequences } from './parseSequences'
import { isSequenceInClade } from './isSequenceInClade'
Expand Down
11 changes: 11 additions & 0 deletions packages/web/src/algorithms/types.ts
Expand Up @@ -42,6 +42,17 @@ export interface Substitutions {
[key: string]: DeepReadonly<NucleotideLocation[]>
}

export interface CladeDataFlat {
cladeName: string
pos: number
nuc: Nucleotide
}

export interface CladeDataGrouped {
pos: number
subs: Record<Nucleotide, string[]>
}

export interface AminoacidSubstitution {
refAA: Aminoacid
queryAA: Aminoacid
Expand Down
53 changes: 53 additions & 0 deletions packages/web/src/components/GeneMap/CladeMarker.tsx
@@ -0,0 +1,53 @@
import React, { SVGProps, useState } from 'react'

import { useTranslation } from 'react-i18next'

import { BASE_MIN_WIDTH_PX } from 'src/constants'

import type { CladeDataGrouped } from 'src/algorithms/types'
import { getSafeId } from 'src/helpers/getSafeId'
import { Tooltip } from 'src/components/Results/Tooltip'

const GENE_MAP_CLADE_MARK_COLOR = '#444444aa' as const

export interface CladeMarkerProps extends SVGProps<SVGRectElement> {
cladeDatum: CladeDataGrouped
pixelsPerBase: number
}

export function CladeMarker({ cladeDatum, pixelsPerBase, ...rest }: CladeMarkerProps) {
const { t } = useTranslation()
const [showTooltip, setShowTooltip] = useState(false)
const { pos, subs } = cladeDatum

const id = getSafeId('clade-marker', { pos })
const fill = GENE_MAP_CLADE_MARK_COLOR
const x = pos * pixelsPerBase
const width = Math.max(BASE_MIN_WIDTH_PX, 1 * pixelsPerBase)

const mutationItems = Object.entries(subs as Record<string, string[]>).map(([nuc, clades]) => {
return <li key={nuc}>{`${nuc}: ${clades.join(', ')}`}</li>
})

return (
<rect
id={id}
fill={fill}
x={x}
y={-10}
width={width}
height="30"
{...rest}
onMouseEnter={() => setShowTooltip(true)}
onMouseLeave={() => setShowTooltip(false)}
>
<Tooltip target={id} isOpen={showTooltip}>
<div>{t('Position: {{position}}', { position: pos })}</div>
<div>{t('Signature mutations:')}</div>
<div>
<ul>{mutationItems}</ul>
</div>
</Tooltip>
</rect>
)
}
78 changes: 5 additions & 73 deletions packages/web/src/components/GeneMap/GeneMap.tsx
@@ -1,20 +1,16 @@
import React, { SVGProps, useState } from 'react'

import { Popover, PopoverBody } from 'reactstrap'
import ReactResizeDetector from 'react-resize-detector'

import { BASE_MIN_WIDTH_PX } from 'src/constants'
import { getSafeId } from 'src/helpers/getSafeId'

import type { Gene, Nucleotide } from 'src/algorithms/types'
import type { Gene } from 'src/algorithms/types'
import { geneMap } from 'src/algorithms/geneMap'
import { VIRUSES } from 'src/algorithms/viruses'
import { cladesGrouped } from 'src/algorithms/clades'

import { GENOME_SIZE } from '../SequenceView/SequenceView'
import { CladeMarker } from './CladeMark'
import { GeneTooltip, getGeneId } from './GeneTooltip'

const GENE_MAP_CLADE_MARK_COLOR = '#444444aa' as const

export interface GeneViewProps extends SVGProps<SVGRectElement> {
gene: Gene
pixelsPerBase: number
Expand All @@ -29,48 +25,8 @@ export function GeneView({ gene, pixelsPerBase, ...rest }: GeneViewProps) {
return <rect id={id} fill={gene.color} x={x} y={-10 + 7.5 * frame} width={width} height="15" {...rest} />
}

export interface CladeMarkProps extends SVGProps<SVGRectElement> {
id: string
pos: number
pixelsPerBase: number
}

export function CladeMark({ id, pos, pixelsPerBase, ...rest }: CladeMarkProps) {
const fill = GENE_MAP_CLADE_MARK_COLOR
const x = pos * pixelsPerBase
const width = Math.max(BASE_MIN_WIDTH_PX, 1 * pixelsPerBase)
return <rect id={id} fill={fill} x={x} y={-10} width={width} height="30" {...rest} />
}

interface CladeMark {
id: string
pos: number
cladeName: string
nuc: Nucleotide
}

export interface CladeMarkTooltipProps {
cladeMark: CladeMark
}

export function CladeMarkTooltip({ cladeMark }: CladeMarkTooltipProps) {
const { id, pos, nuc, cladeName } = cladeMark

return (
<Popover className="popover-mutation" target={id} placement="auto" isOpen hideArrow delay={0} fade={false}>
<PopoverBody>
<div>{`Clade: ${cladeName} `}</div>
<div>{`Position: ${pos} `}</div>
<div>{`Nucleotide: ${nuc} `}</div>
</PopoverBody>
</Popover>
)
}

export function GeneMap() {
const [currGene, setCurrGene] = useState<Gene | undefined>(undefined)
const [currCladeMark, setCurrCladeMark] = useState<CladeMark | undefined>(undefined)
const { clades } = VIRUSES['SARS-CoV-2']

return (
<ReactResizeDetector handleWidth refreshRate={300} refreshMode="debounce">
Expand All @@ -92,31 +48,8 @@ export function GeneMap() {
)
})

// TODO: move to algorithms
const cladeSubstitutions = Object.entries(clades).reduce((result, clade) => {
const [cladeName, substitutions] = clade

const marks: CladeMark[] = substitutions.map((substitution) => {
const id = getSafeId('clade-mark', { cladeName, ...substitution })
const { pos, nuc } = substitution
return { id, cladeName, pos, nuc }
})

return [...result, ...marks]
}, [] as CladeMark[])

const cladeMarks = cladeSubstitutions.map((cladeSubstitution) => {
const { id, pos } = cladeSubstitution
return (
<CladeMark
key={id}
id={id}
pos={pos}
pixelsPerBase={pixelsPerBase}
onMouseEnter={() => setCurrCladeMark(cladeSubstitution)}
onMouseLeave={() => setCurrCladeMark(undefined)}
/>
)
const cladeMarks = cladesGrouped.map((cladeDatum) => {
return <CladeMarker key={cladeDatum.pos} cladeDatum={cladeDatum} pixelsPerBase={pixelsPerBase} />
})

return (
Expand All @@ -127,7 +60,6 @@ export function GeneMap() {
{cladeMarks}
</svg>
{currGene && <GeneTooltip gene={currGene} />}
{currCladeMark && <CladeMarkTooltip cladeMark={currCladeMark} />}
</div>
)
}}
Expand Down

0 comments on commit 6971f9c

Please sign in to comment.