From 6971f9c9495d5481e324bf4f42fe2b63252a1929 Mon Sep 17 00:00:00 2001 From: ivan-aksamentov Date: Mon, 29 Jun 2020 16:08:10 +0200 Subject: [PATCH] feat: group clades in mutation tooltips on gene map --- packages/web/src/algorithms/clades.ts | 40 ++++++++++ packages/web/src/algorithms/run.ts | 4 +- packages/web/src/algorithms/types.ts | 11 +++ .../src/components/GeneMap/CladeMarker.tsx | 53 +++++++++++++ .../web/src/components/GeneMap/GeneMap.tsx | 78 ++----------------- 5 files changed, 111 insertions(+), 75 deletions(-) create mode 100644 packages/web/src/algorithms/clades.ts create mode 100644 packages/web/src/components/GeneMap/CladeMarker.tsx diff --git a/packages/web/src/algorithms/clades.ts b/packages/web/src/algorithms/clades.ts new file mode 100644 index 000000000..c8926e1b9 --- /dev/null +++ b/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 + return { pos, subs } + }) + + return uniqBy(cladesGroupedByNuc, ({ pos }) => pos) +} + +export const cladesGrouped = groupClades(VIRUSES['SARS-CoV-2'].clades) diff --git a/packages/web/src/algorithms/run.ts b/packages/web/src/algorithms/run.ts index 4e1a5facf..f7774dbff 100644 --- a/packages/web/src/algorithms/run.ts +++ b/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' diff --git a/packages/web/src/algorithms/types.ts b/packages/web/src/algorithms/types.ts index d8e784606..fe8cfed02 100644 --- a/packages/web/src/algorithms/types.ts +++ b/packages/web/src/algorithms/types.ts @@ -42,6 +42,17 @@ export interface Substitutions { [key: string]: DeepReadonly } +export interface CladeDataFlat { + cladeName: string + pos: number + nuc: Nucleotide +} + +export interface CladeDataGrouped { + pos: number + subs: Record +} + export interface AminoacidSubstitution { refAA: Aminoacid queryAA: Aminoacid diff --git a/packages/web/src/components/GeneMap/CladeMarker.tsx b/packages/web/src/components/GeneMap/CladeMarker.tsx new file mode 100644 index 000000000..8e6d08cbb --- /dev/null +++ b/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 { + 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).map(([nuc, clades]) => { + return
  • {`${nuc}: ${clades.join(', ')}`}
  • + }) + + return ( + setShowTooltip(true)} + onMouseLeave={() => setShowTooltip(false)} + > + +
    {t('Position: {{position}}', { position: pos })}
    +
    {t('Signature mutations:')}
    +
    +
      {mutationItems}
    +
    +
    +
    + ) +} diff --git a/packages/web/src/components/GeneMap/GeneMap.tsx b/packages/web/src/components/GeneMap/GeneMap.tsx index a787c8f58..0590f98de 100644 --- a/packages/web/src/components/GeneMap/GeneMap.tsx +++ b/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 { gene: Gene pixelsPerBase: number @@ -29,48 +25,8 @@ export function GeneView({ gene, pixelsPerBase, ...rest }: GeneViewProps) { return } -export interface CladeMarkProps extends SVGProps { - 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 -} - -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 ( - - -
    {`Clade: ${cladeName} `}
    -
    {`Position: ${pos} `}
    -
    {`Nucleotide: ${nuc} `}
    -
    -
    - ) -} - export function GeneMap() { const [currGene, setCurrGene] = useState(undefined) - const [currCladeMark, setCurrCladeMark] = useState(undefined) - const { clades } = VIRUSES['SARS-CoV-2'] return ( @@ -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 ( - setCurrCladeMark(cladeSubstitution)} - onMouseLeave={() => setCurrCladeMark(undefined)} - /> - ) + const cladeMarks = cladesGrouped.map((cladeDatum) => { + return }) return ( @@ -127,7 +60,6 @@ export function GeneMap() { {cladeMarks} {currGene && } - {currCladeMark && } ) }}