Skip to content

Commit

Permalink
feat: start re-implementing msa viewer
Browse files Browse the repository at this point in the history
  • Loading branch information
ivan-aksamentov committed Jan 18, 2023
1 parent a28f874 commit 26af64c
Show file tree
Hide file tree
Showing 71 changed files with 334 additions and 6,323 deletions.
283 changes: 196 additions & 87 deletions src/components/Msa/Msa.tsx
Original file line number Diff line number Diff line change
@@ -1,101 +1,210 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
import React, { useEffect, useRef } from 'react'
import React, { ComponentProps, Suspense, useMemo } from 'react'
import { Stage, Layer, Rect, Text, Group } from 'react-konva'
import { useResizeDetector } from 'react-resize-detector'
import { CardHeader, Card, Col, Container, Row, CardBody } from 'reactstrap'
import styled, { useTheme } from 'styled-components'
import { getAminoacidColor } from 'src/helpers/getAminoacidColor'
import { getTextColor } from 'src/helpers/getTextColor'
import { useTranslationSafe } from 'src/helpers/useTranslationSafe'
import type { GeneCluster, SpeciesDesc } from 'src/hooks/useDataIndexQuery'
import { SequenceType, useGeneClusterData } from 'src/hooks/useDataIndexQuery'
import { FastaEntry, Mutation, parseFastaToRefAndMutations, SequenceEntry } from 'src/io/parseFasta'
import { LOADING } from 'src/components/Loading/Loading'
import { getNucleotideColor } from 'src/helpers/getNucleotideColor'

import { GeneCluster, SpeciesDesc, useGeneClusterData } from 'src/hooks/useDataIndexQuery'

// @ts-ignore
import msa from 'src/components/Msa/msa/src/msa'

// import { panXTree, msaViewerAsset } from '../Tree/global'
// import {hideNonSelected} from './tree-init'
// import {button_tooltip,append_download_button} from './tooltips'
const MsaContainer = styled(Container)`
height: 600px;
`

export interface MsaProps {
species: SpeciesDesc
gene: GeneCluster
seqType: string
seqType: SequenceType
species: SpeciesDesc
}

export default function Msa(props: MsaProps) {
const { t } = useTranslationSafe()
return (
<MsaContainer fluid>
<Row noGutters className="w-100 h-100 m-0 p-0">
<Col className="m-0 p-0 pr-1">
<Card className="h-100">
<CardHeader>
<h4>{t('Sequences')}</h4>
</CardHeader>
<CardBody>
<Suspense fallback={LOADING}>
<MsaImpl {...props} />
</Suspense>
</CardBody>
</Card>
</Col>
</Row>
</MsaContainer>
)
}

export default function Msa({ species, gene, seqType }: MsaProps) {
const ref = useRef<HTMLDivElement>(null)
function MsaImpl({ species, gene, seqType }: MsaProps) {
const {
width,
height,
ref: containerRef,
} = useResizeDetector({
handleHeight: true,
refreshOptions: { leading: true, trailing: true },
})

return (
<div className="w-100 h-100" ref={containerRef}>
<MsaSized width={width} height={height} species={species} gene={gene} seqType={seqType} />
</div>
)
}

export interface MsaSizedProps extends MsaProps {
width?: number
height?: number
}

function MsaSized({ species, gene, seqType, width, height }: MsaSizedProps) {
const { aa_aln_reduced, na_aln_reduced } = useGeneClusterData(species, gene)

useEffect(() => {
if (!ref.current) {
return
const data = useMemo(() => {
const fasta = seqType === SequenceType.Aa ? aa_aln_reduced : na_aln_reduced
if (!fasta) {
return null
}
return parseFastaToRefAndMutations(fasta)
}, [aa_aln_reduced, na_aln_reduced, seqType])

const fasta = seqType === 'aa' ? aa_aln_reduced : na_aln_reduced
if (!fasta) {
return
const rows = useMemo(() => {
if (!data) {
return null
}

msaLoad(ref.current, seqType, fasta)
}, [aa_aln_reduced, na_aln_reduced, seqType, species.id])

return <div ref={ref} />
}

function msaLoad(rootDiv: HTMLDivElement, seqType: string, fasta: string) {
// eslint-disable-next-line new-cap
const m = new msa({
el: rootDiv,
importFasta: fasta,
bootstrapMenu: false,
vis: { scaleslider: true, conserv: false, overviewbox: false, labelId: false },
zoomer: { labelFontsize: '12', residueFont: '12' },
/* zoomer: {
alignmentWidth:'auto',alignmentHeight: 250,rowHeight: 18,
labelWidth: 100, labelNameLength: 150,
labelNameFontsize: '10px',labelIdLength: 20, menuFontsize: '12px',
menuMarginLeft: '3px', menuPadding: '3px 4px 3px 4px', menuItemFontsize: '14px', menuItemLineHeight: '14px',
boxRectHeight: 2, boxRectWidth: 0.1, overviewboxPaddingTop: 20
};*/
colorscheme: { scheme: seqType === 'aa' ? 'taylor' : 'nucleotide' },
})
const { refEntry, entries } = data

return entries.map((entry, i) => (
<MsaRow key={entry.index} refEntry={refEntry} entry={entry} y={1 + MSA_CHAR_HEIGHT * i} seqType={seqType} />
))
}, [data, seqType])

if (!data) {
return null
}

return (
<Stage width={width} height={height}>
<Layer clearBeforeDraw>
<Group>
<MsaRefRow refEntry={data.refEntry} seqType={seqType} />
{rows}
</Group>
</Layer>
</Stage>
)
}

const MSA_CHAR_HEIGHT = 20
const MSA_CHAR_WIDTH = 20
const MSA_CHAR_FONT_SIZE = 14

export interface MsaRefRowProps {
refEntry: FastaEntry
seqType: SequenceType
}

function MsaRefRow({ refEntry, seqType }: MsaRefRowProps) {
const chars = useMemo(() => <MsaSequence seq={refEntry.seq} seqType={seqType} />, [refEntry.seq, seqType])
return <Group>{chars}</Group>
}

export interface MsaRowProps extends ComponentProps<typeof Group> {
refEntry: FastaEntry
entry: SequenceEntry
mutationsOnly?: boolean
seqType: SequenceType
}

function MsaRow({ refEntry, entry, mutationsOnly, seqType, ...restProps }: MsaRowProps) {
const component = useMemo(() => {
const mutations = entry.mutations.filter((mut) => mut.pos < refEntry.seq.length)
if (mutationsOnly) {
return <MsaSequence seq={applyMutations(refEntry.seq, mutations)} seqType={seqType} />
}
return <MsaMutations mutations={mutations} seqType={seqType} />
}, [entry.mutations, mutationsOnly, refEntry.seq, seqType])
return <Group {...restProps}>{component}</Group>
}

function applyMutations(refSeq: string, mutations: Mutation[]) {
const seq = refSeq.split('')
mutations
.filter((mut) => mut.pos < refSeq.length)
.forEach(({ pos, qry }) => {
seq[pos] = qry
})
return seq.join('')
}

export interface MsaMutationsProps extends ComponentProps<typeof Group> {
mutations: Mutation[]
seqType: SequenceType
}

function MsaMutations({ mutations, seqType, ...restProps }: MsaMutationsProps) {
const chars = useMemo(
() =>
mutations.map(({ pos, qry }) => (
<MsaCharacter key={pos} x={MSA_CHAR_WIDTH * pos} y={0} character={qry} seqType={seqType} />
)),
[mutations, seqType],
)
return <Group {...restProps}>{chars}</Group>
}

export interface MsaSequenceProps extends ComponentProps<typeof Group> {
seq: string
seqType: SequenceType
}

function MsaSequence({ seq, seqType, ...restProps }: MsaSequenceProps) {
const chars = useMemo(
() =>
seq.split('').map((c, pos) => (
// eslint-disable-next-line react/no-array-index-key
<MsaCharacter key={`${c}-${pos}`} x={MSA_CHAR_WIDTH * pos} y={0} character={c} seqType={seqType} />
)),
[seq, seqType],
)
return <Group {...restProps}>{chars}</Group>
}

export interface MsaCharacterProps extends ComponentProps<typeof Group> {
character: string
seqType: SequenceType
}

function MsaCharacter({ character, seqType, ...restProps }: MsaCharacterProps) {
const theme = useTheme()
const { textColor, fillColor } = useMemo(() => {
const fillColor = seqType === SequenceType.Aa ? getAminoacidColor(character) : getNucleotideColor(character)
return { textColor: getTextColor(theme, fillColor), fillColor }
}, [character, seqType, theme])

// //# click row/rows to highlight the related strain/strains
// const seqID_to_accession_dt = {}
// m.g.on('row:click', (data) => {
// const alnID = data.evt.currentTarget.textContent
// const accession = alnID.split('-', 1)[0]
// seqID_to_accession_dt[data.seqId] = accession
// msaViewerAsset.selected_rows_set = new Set()
// const selection = m.g.selcol.pluck('seqId')
// for (let i = 0, len = selection.length; i < len; i++) {
// msaViewerAsset.selected_rows_set.add(seqID_to_accession_dt[selection[i]])
// }
// // var speciesTree = panXTree.speciesTree
// //
// // if (speciesTree) {
// // speciesTree.tips.forEach(function (d) {
// // d.state.selected = false
// // })
// // msaViewerAsset.selected_rows_set.forEach(function (accession) {
// // if (speciesTree.namesToTips[accession]) {
// // speciesTree.namesToTips[accession].state.selected = true
// // } else {
// // console.log('accession not found', accession)
// // }
// // })
// // } else {
// // console.log('speciesTree not available')
// // }
// // hideNonSelected(speciesTree)
// })
//
// const scaleSize = seqType === 'nuc' ? 2 : 3
// m.g.scale.setSize(scaleSize)

// const msa_legend = d3.select('#msa_legend')
// msa_legend.selectAll('a, span').remove()
// append_download_button('#msa_legend', 'msa_aln', aln_path.replace('_reduced', ''))
// append_download_button('#msa_legend', 'msa_reduced_aln', aln_path)
// const msa_button_tooltip_dict = {
// msa_aln: 'download current alignment',
// msa_reduced_aln: 'download current reduced alignment (consensus sequence and variable sites)',
// }
// button_tooltip('#msa_legend', msa_button_tooltip_dict)

m.render()
return (
<Group {...restProps}>
<Rect width={MSA_CHAR_WIDTH} height={MSA_CHAR_HEIGHT} fill={fillColor} strokeWidth={0.5} stroke="#ffffffaa" />
<Text
width={MSA_CHAR_WIDTH}
height={MSA_CHAR_HEIGHT}
fill={textColor}
text={character}
fontSize={MSA_CHAR_FONT_SIZE}
fontFamily="monospace"
align="center"
verticalAlign="middle"
/>
</Group>
)
}
23 changes: 0 additions & 23 deletions src/components/Msa/msa/LICENSE

This file was deleted.

6 changes: 0 additions & 6 deletions src/components/Msa/msa/README.md

This file was deleted.

Loading

1 comment on commit 26af64c

@vercel
Copy link

@vercel vercel bot commented on 26af64c Jan 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

pangenome – ./

pangenome-neherlab.vercel.app
pangenome-git-react-app-neherlab.vercel.app

Please sign in to comment.