Skip to content

Commit

Permalink
feat: make msa canvas scrollable
Browse files Browse the repository at this point in the history
  • Loading branch information
ivan-aksamentov committed Jan 18, 2023
1 parent 9d10235 commit 3f03485
Show file tree
Hide file tree
Showing 7 changed files with 121 additions and 72 deletions.
98 changes: 78 additions & 20 deletions src/components/Msa/Msa.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,31 @@
import React, { Suspense, useMemo } from 'react'
import { Group, Layer, Stage } from 'react-konva'
import React, { Suspense, UIEvent, useCallback, useMemo, useRef } from 'react'
import Konva from 'konva'
import { clamp } from 'lodash'
import { Group, Layer, Stage as StageBase } from 'react-konva'
import { useResizeDetector } from 'react-resize-detector'
import styled from 'styled-components'
import { Card, CardBody, CardHeader, Col, Container, Row } from 'reactstrap'
import { LOADING } from 'src/components/Loading/Loading'
import { MSA_CHAR_HEIGHT } from 'src/components/Msa/MsaCharacter'
import { MSA_CHAR_HEIGHT, MSA_CHAR_WIDTH } from 'src/components/Msa/MsaCharacter'
import { MsaRow } from 'src/components/Msa/MsaRow'
import { MsaSequence } from 'src/components/Msa/MsaSequence'
import { useTranslationSafe } from 'src/helpers/useTranslationSafe'
import type { GeneCluster, SpeciesDesc } from 'src/hooks/useDataIndexQuery'
import { SequenceType, useGeneClusterData } from 'src/hooks/useDataIndexQuery'
import { parseFastaToRefAndMutations } from 'src/io/parseFasta'
import styled from 'styled-components'

const MsaContainer = styled(Container)`
height: 600px;
height: 300px;
max-height: 300px;
`

export interface MsaProps {
gene: GeneCluster
seqType: SequenceType
species: SpeciesDesc
aspectRatio?: number
maxWidth?: number
maxHeight?: number
}

export default function Msa(props: MsaProps) {
Expand All @@ -44,7 +50,7 @@ export default function Msa(props: MsaProps) {
)
}

function MsaImpl({ species, gene, seqType }: MsaProps) {
function MsaImpl({ maxWidth, maxHeight, aspectRatio, ...restProps }: MsaProps) {
const {
width,
height,
Expand All @@ -54,16 +60,19 @@ function MsaImpl({ species, gene, seqType }: MsaProps) {
refreshOptions: { leading: true, trailing: true },
})

const adjustedWidth = clamp(width ?? 0, 0, maxWidth ?? Number.POSITIVE_INFINITY)
const adjustedHeight = clamp(adjustedWidth / (aspectRatio ?? 10), height ?? 0, maxHeight ?? Number.POSITIVE_INFINITY)

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

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

function MsaSized({ species, gene, seqType, width, height }: MsaSizedProps) {
Expand All @@ -77,30 +86,79 @@ function MsaSized({ species, gene, seqType, width, height }: MsaSizedProps) {
return parseFastaToRefAndMutations(fasta)
}, [aa_aln_reduced, na_aln_reduced, seqType])

const rows = useMemo(() => {
const { rows, numChars } = useMemo(() => {
if (!data) {
return null
return { rows: [], numChars: 0 }
}

const { refEntry, entries } = data

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

const numChars = refEntry.seq.length
return { rows, numChars }
}, [data, seqType])

const scrollContainer = useRef<HTMLDivElement>(null)
const stage = useRef<Konva.Stage>(null)

const onScroll = useCallback((_e: UIEvent<HTMLDivElement>) => {
if (scrollContainer.current) {
const { scrollLeft, scrollTop } = scrollContainer.current

const dx = scrollLeft - PADDING
const dy = scrollTop - PADDING

if (stage.current) {
stage.current.container().style.transform = `translate(${dx}px, ${dy}px)`
stage.current.x(-dx)
stage.current.y(-dy)
}
}
}, [])

if (!data) {
return null
}

const largeWidth = MSA_CHAR_WIDTH * numChars
const largeHeight = MSA_CHAR_HEIGHT * rows.length

return (
<Stage width={width} height={height}>
<Layer clearBeforeDraw>
<Group>
<MsaSequence seq={data.refEntry.seq} seqType={seqType} />
{rows}
</Group>
</Layer>
</Stage>
<MsaScrollContainer $width={width} $height={height} ref={scrollContainer} onScroll={onScroll}>
<MsaLargeContainer $width={largeWidth} $height={largeHeight}>
<Stage width={width + PADDING} height={height + PADDING} ref={stage}>
<Layer clearBeforeDraw>
<Group>
<MsaSequence seq={data.refEntry.seq} seqType={seqType} />
{rows}
</Group>
</Layer>
</Stage>
</MsaLargeContainer>
</MsaScrollContainer>
)
}

const PADDING = 100

const MsaLargeContainer = styled.div<{ $width: number; $height: number }>`
margin: 0;
padding: 0;
width: ${(props) => props.$width}px;
height: ${(props) => props.$height}px;
overflow: hidden;
`

const MsaScrollContainer = styled.div<{ $width: number; $height: number }>`
width: ${(props) => props.$width}px;
height: ${(props) => props.$height}px;
overflow: scroll;
`

const Stage = styled(StageBase)`
position: relative;
outline: none;
`
16 changes: 13 additions & 3 deletions src/components/Msa/MsaCharacter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,12 @@ export function MsaCharacter({ character, seqType, ...restProps }: MsaCharacterP
return { textColor: getTextColor(theme, fillColor), fillColor }
}, [character, seqType, theme])

return (
<Group {...restProps}>
<Rect width={MSA_CHAR_WIDTH} height={MSA_CHAR_HEIGHT} fill={fillColor} strokeWidth={0.5} stroke="#ffffffaa" />
const text = useMemo(() => {
if (character === ' ') {
return null
}

return (
<Text
width={MSA_CHAR_WIDTH}
height={MSA_CHAR_HEIGHT}
Expand All @@ -35,6 +38,13 @@ export function MsaCharacter({ character, seqType, ...restProps }: MsaCharacterP
align="center"
verticalAlign="middle"
/>
)
}, [character, textColor])

return (
<Group {...restProps}>
<Rect width={MSA_CHAR_WIDTH} height={MSA_CHAR_HEIGHT} fill={fillColor} strokeWidth={0.5} stroke="#ccca" />
{text}
</Group>
)
}
21 changes: 0 additions & 21 deletions src/components/Msa/MsaMutations.tsx

This file was deleted.

4 changes: 2 additions & 2 deletions src/components/Msa/MsaRow.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import React, { ComponentProps, useMemo } from 'react'
import { Group } from 'react-konva'
import { MsaMutations } from 'src/components/Msa/MsaMutations'
import { MsaSequence } from 'src/components/Msa/MsaSequence'
import { SequenceType } from 'src/hooks/useDataIndexQuery'
import { FastaEntry, Mutation, SequenceEntry } from 'src/io/parseFasta'
Expand All @@ -18,7 +17,8 @@ export function MsaRow({ refEntry, entry, mutationsOnly, seqType, ...restProps }
if (mutationsOnly) {
return <MsaSequence seq={applyMutations(refEntry.seq, mutations)} seqType={seqType} />
}
return <MsaMutations mutations={mutations} seqType={seqType} />
const blank = ' '.repeat(refEntry.seq.length)
return <MsaSequence seq={applyMutations(blank, mutations)} seqType={seqType} />
}, [entry.mutations, mutationsOnly, refEntry.seq, seqType])
return <Group {...restProps}>{component}</Group>
}
Expand Down
50 changes: 26 additions & 24 deletions src/components/Species/SpeciesPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@ import { useRecoilValue } from 'recoil'
import type { GeneCluster, SpeciesDesc } from 'src/hooks/useDataIndexQuery'
import { SequenceType, useGeneClusterData, useGeneClusterJson } from 'src/hooks/useDataIndexQuery'
import { currentGeneIdAtom } from 'src/state/genes'
import { GeneClustersTable } from 'src/components/Species/GeneClustersTable'
import { LOADING } from 'src/components/Loading/Loading'
import { Layout } from 'src/components/Layout/Layout'
import { MetadataTable } from 'src/components/Species/MetadataTable'
// import { GeneClustersTable } from 'src/components/Species/GeneClustersTable'
// import { MetadataTable } from 'src/components/Species/MetadataTable'

const Msa = dynamic(() => import('src/components/Msa/Msa'), { suspense: true, ssr: false })
const Tree = dynamic(() => import('src/components/Tree/Tree'), { suspense: true, ssr: false })

// const Tree = dynamic(() => import('src/components/Tree/Tree'), { suspense: true, ssr: false })

export interface SpeciesPageProps {
species: SpeciesDesc
Expand All @@ -29,7 +30,7 @@ export function SpeciesPage({ species }: SpeciesPageProps) {
}

export function SpeciesInfo({ species }: SpeciesPageProps) {
const geneClusterJson = useGeneClusterJson(species.id)
// const geneClusterJson = useGeneClusterJson(species.id)

return (
<Container fluid>
Expand All @@ -39,11 +40,11 @@ export function SpeciesInfo({ species }: SpeciesPageProps) {
</Col>
</Row>

<Row noGutters>
<Col>
<GeneClustersTable species={species} clusters={geneClusterJson.clusters} />
</Col>
</Row>
{/*<Row noGutters>*/}
{/* <Col>*/}
{/* <GeneClustersTable species={species} clusters={geneClusterJson.clusters} />*/}
{/* </Col>*/}
{/*</Row>*/}

<Row noGutters className="my-4">
<Col>
Expand Down Expand Up @@ -77,22 +78,23 @@ export function GeneClustersSection({ species }: GeneClustersSectionProps) {
</Col>
</Row>

<Row noGutters className="mb-2">
<Col>
<Tree species={species} gene={gene} />
</Col>
</Row>
{/*<Row noGutters className="mb-2">*/}
{/* <Col>*/}
{/* <Tree species={species} gene={gene} />*/}
{/* </Col>*/}
{/*</Row>*/}

<Row noGutters className="mb-2">
<Col>
<MetadataTable species={species} />
</Col>
</Row>
<Row noGutters className="mb-2">
<Col>
<GeneClustersData species={species} gene={gene} />
</Col>
</Row>
{/*<Row noGutters className="mb-2">*/}
{/* <Col>*/}
{/* <MetadataTable species={species} />*/}
{/* </Col>*/}
{/*</Row>*/}

{/*<Row noGutters className="mb-2">*/}
{/* <Col>*/}
{/* <GeneClustersData species={species} gene={gene} />*/}
{/* </Col>*/}
{/*</Row>*/}
</Container>
</Suspense>
)
Expand Down
2 changes: 1 addition & 1 deletion src/helpers/getAminoacidColor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,5 @@ export const AMINOACID_COLORS: Record<string, string> = {
export const AMINOACID_GAP_COLOR = AMINOACID_COLORS['-']

export function getAminoacidColor(aa: string): string {
return get(AMINOACID_COLORS, aa) ?? AMINOACID_COLORS['-']
return get(AMINOACID_COLORS, aa) ?? 'transparent'
}
2 changes: 1 addition & 1 deletion src/helpers/getNucleotideColor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,5 @@ export const NUCLEOTIDE_COLORS: Record<string, string> = {
} as const

export function getNucleotideColor(nuc: string) {
return get(NUCLEOTIDE_COLORS, nuc) ?? NUCLEOTIDE_COLORS.N
return get(NUCLEOTIDE_COLORS, nuc) ?? 'transparent'
}

1 comment on commit 3f03485

@vercel
Copy link

@vercel vercel bot commented on 3f03485 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-git-react-app-neherlab.vercel.app
pangenome-neherlab.vercel.app

Please sign in to comment.