forked from wdingx/pan-genome-visualization
-
Notifications
You must be signed in to change notification settings - Fork 14
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: start re-implementing msa viewer
- Loading branch information
1 parent
a28f874
commit 26af64c
Showing
71 changed files
with
334 additions
and
6,323 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
) | ||
} |
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.
26af64c
There was a problem hiding this comment.
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