Skip to content

Commit

Permalink
feat(web): add sequence sorting prototype
Browse files Browse the repository at this point in the history
  • Loading branch information
ivan-aksamentov committed Sep 5, 2023
1 parent db37dd3 commit 8ac3f67
Show file tree
Hide file tree
Showing 23 changed files with 520 additions and 62 deletions.
8 changes: 1 addition & 7 deletions packages_rs/nextclade-cli/src/cli/nextclade_seq_sort.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,14 @@ use log::{info, trace, LevelFilter};
use nextclade::io::fasta::{FastaReader, FastaRecord, FastaWriter};
use nextclade::make_error;
use nextclade::sort::minimizer_index::{MinimizerIndexJson, MINIMIZER_INDEX_ALGO_VERSION};
use nextclade::sort::minimizer_search::{run_minimizer_search, MinimizerSearchResult};
use nextclade::sort::minimizer_search::{run_minimizer_search, MinimizerSearchRecord};
use nextclade::utils::string::truncate;
use serde::Serialize;
use std::collections::BTreeMap;
use std::path::PathBuf;
use std::str::FromStr;
use tinytemplate::TinyTemplate;

#[derive(Debug, Clone)]
struct MinimizerSearchRecord {
pub fasta_record: FastaRecord,
pub result: MinimizerSearchResult,
}

pub fn nextclade_seq_sort(args: &NextcladeSeqSortArgs) -> Result<(), Report> {
check_args(args)?;

Expand Down
5 changes: 5 additions & 0 deletions packages_rs/nextclade-web/src/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ use nextclade::qc::qc_run::QcResult;
use nextclade::run::nextclade_wasm::{
AnalysisInitialData, AnalysisInput, NextcladeParams, NextcladeParamsRaw, NextcladeResult, OutputTrees,
};
use nextclade::sort::minimizer_index::MinimizerIndexJson;
use nextclade::sort::minimizer_search::{MinimizerSearchRecord, MinimizerSearchResult};
use nextclade::translate::translate_genes::Translation;
use nextclade::tree::tree::{AuspiceTree, CladeNodeAttrKeyDesc};
use nextclade::types::outputs::{NextcladeErrorOutputs, NextcladeOutputs};
Expand Down Expand Up @@ -73,4 +75,7 @@ struct _SchemaRoot<'a> {
_27: DatasetAttributeValue,
_28: DatasetAttributes,
_29: DatasetCollectionUrl,
_30: MinimizerIndexJson,
_31: MinimizerSearchResult,
_32: MinimizerSearchRecord,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import React from 'react'
import { Col as ColBase, Row as RowBase } from 'reactstrap'
import { useRecoilValue } from 'recoil'
import { TableSlimWithBorders } from 'src/components/Common/TableSlim'
import { Layout } from 'src/components/Layout/Layout'
import { useTranslationSafe } from 'src/helpers/useTranslationSafe'
import { autodetectResultsAtom } from 'src/state/autodetect.state'
import styled from 'styled-components'

const Container = styled.div`
margin-top: 1rem;
height: 100%;
overflow: hidden;
`

const Row = styled(RowBase)`
overflow: hidden;
height: 100%;
`

const Col = styled(ColBase)`
overflow: hidden;
height: 100%;
`

const Table = styled(TableSlimWithBorders)`
padding-top: 50px;
& thead {
height: 51px;
position: sticky;
top: -2px;
background-color: ${(props) => props.theme.gray700};
color: ${(props) => props.theme.gray100};
}
& thead th {
margin: auto;
text-align: center;
vertical-align: middle;
}
`

const TableWrapper = styled.div`
height: 100%;
overflow-y: auto;
`

export function AutodetectPage() {
const { t } = useTranslationSafe()
// const minimizerIndex = useRecoilValue(minimizerIndexAtom)
const autodetectResults = useRecoilValue(autodetectResultsAtom)

return (
<Layout>
<Container>
<Row noGutters>
<Col>
<TableWrapper>
<Table striped>
<thead>
<tr>
<th>{'#'}</th>
<th>{t('Seq. name')}</th>
<th>{t('dataset')}</th>
<th>{t('total hits')}</th>
<th>{t('max hit')}</th>
</tr>
</thead>

<tbody>
{autodetectResults.map((res) => (
<tr key={res.fastaRecord.index}>
<td>{res.fastaRecord.index}</td>
<td>{res.fastaRecord.seqName}</td>
<td>{res.result.dataset ?? ''}</td>
<td>{res.result.totalHits}</td>
<td>{res.result.maxNormalizedHit.toFixed(3)}</td>
</tr>
))}
</tbody>
</Table>
</TableWrapper>
</Col>
</Row>
</Container>
</Layout>
)
}
44 changes: 24 additions & 20 deletions packages_rs/nextclade-web/src/components/Main/DatasetCurrent.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { isNil } from 'lodash'
import React, { useCallback, useState } from 'react'
import React, { useCallback, useMemo, useState } from 'react'
import { Button, Col, Collapse, Row, UncontrolledAlert } from 'reactstrap'
import { useRecoilState, useRecoilValue, useResetRecoilState, useSetRecoilState } from 'recoil'
import styled from 'styled-components'
Expand Down Expand Up @@ -82,6 +82,28 @@ export function DatasetCurrent() {

const onCustomizeClicked = useCallback(() => setAdvancedOpen((advancedOpen) => !advancedOpen), [])

const customize = useMemo(() => {
if (datasetCurrent?.path === 'autodetect') {
return null
}

return (
<Row noGutters>
<Col>
<ButtonCustomize isOpen={advancedOpen} onClick={onCustomizeClicked} />

<Collapse isOpen={advancedOpen}>
<AdvancedModeExplanationWrapper>
<AdvancedModeExplanationContent />
</AdvancedModeExplanationWrapper>

<FilePickerAdvanced />
</Collapse>
</Col>
</Row>
)
}, [advancedOpen, datasetCurrent?.path, onCustomizeClicked])

if (!datasetCurrent) {
return null
}
Expand All @@ -105,29 +127,11 @@ export function DatasetCurrent() {
<ChangeButton type="button" color="secondary" onClick={onChangeClicked}>
{t('Change')}
</ChangeButton>
<LinkExternal
className="ml-auto mt-auto"
href="https://github.com/nextstrain/nextclade_data/blob/release/CHANGELOG.md"
>
<small>{t('Recent dataset updates')}</small>
</LinkExternal>
</Right>
</Col>
</Row>

<Row noGutters>
<Col>
<ButtonCustomize isOpen={advancedOpen} onClick={onCustomizeClicked} />

<Collapse isOpen={advancedOpen}>
<AdvancedModeExplanationWrapper>
<AdvancedModeExplanationContent />
</AdvancedModeExplanationWrapper>

<FilePickerAdvanced />
</Collapse>
</Col>
</Row>
{customize}
</CurrentDatasetInfoBody>
</CurrentDatasetInfoContainer>
)
Expand Down
24 changes: 24 additions & 0 deletions packages_rs/nextclade-web/src/components/Main/DatasetInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ export const DatasetInfoLine = styled.p`
font-size: 0.9rem;
padding: 0;
margin: 0;
&:after {
content: ' ';
white-space: pre;
}
`

const DatasetInfoBadge = styled(Badge)`
Expand Down Expand Up @@ -50,6 +55,10 @@ export function DatasetInfo({ dataset }: DatasetInfoProps) {
return null
}

if (path === 'autodetect') {
return <DatasetAutodetectInfo />
}

return (
<DatasetInfoContainer>
<DatasetName>
Expand Down Expand Up @@ -107,3 +116,18 @@ export function DatasetInfo({ dataset }: DatasetInfoProps) {
</DatasetInfoContainer>
)
}

export function DatasetAutodetectInfo() {
const { t } = useTranslationSafe()

return (
<DatasetInfoContainer>
<DatasetName>
<span>{t('Autodetect')}</span>
</DatasetName>
<DatasetInfoLine>{t('Detect pathogen automatically from sequences')}</DatasetInfoLine>
<DatasetInfoLine />
<DatasetInfoLine />
</DatasetInfoContainer>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,20 @@ import { DatasetInfo } from 'src/components/Main/DatasetInfo'
// border-radius: 5px;
// `

const DATASET_AUTODETECT: Dataset = {
path: 'autodetect',
enabled: true,
official: true,
attributes: {
name: { value: 'autodetect', valueFriendly: 'Autodetect' },
reference: { value: 'autodetect', valueFriendly: 'Autodetect' },
},
files: {
reference: '',
pathogenJson: '',
},
}

export const DatasetSelectorUl = styled(ListGroup)`
flex: 1;
overflow-y: scroll;
Expand Down Expand Up @@ -79,6 +93,14 @@ export function DatasetSelectorList({
return (
// <DatasetSelectorContainer>
<DatasetSelectorUl>
{
<DatasetSelectorListItem
dataset={DATASET_AUTODETECT}
onClick={onItemClick(DATASET_AUTODETECT)}
isCurrent={areDatasetsEqual(DATASET_AUTODETECT, datasetHighlighted)}
/>
}

{[itemsStartWith, itemsInclude].map((datasets) =>
datasets.map((dataset) => (
<DatasetSelectorListItem
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Button, Col, Form, FormGroup, Row } from 'reactstrap'
import { useRecoilState, useRecoilValue } from 'recoil'
import { MainInputFormSequencesCurrent } from 'src/components/Main/MainInputFormSequencesCurrent'
import { useRunAnalysis } from 'src/hooks/useRunAnalysis'
import { useRunSeqAutodetect } from 'src/hooks/useRunSeqAutodetect'
import { canRunAtom } from 'src/state/results.state'
import styled from 'styled-components'

Expand Down Expand Up @@ -46,12 +47,20 @@ export function MainInputFormSequenceFilePicker() {

const icon = useMemo(() => <FileIconFasta />, [])

const run = useRunAnalysis()
const runAnalysis = useRunAnalysis()
const runAutodetect = useRunSeqAutodetect()

const run = useCallback(() => {
if (datasetCurrent?.path === 'autodetect') {
runAutodetect()
} else {
runAnalysis()
}
}, [datasetCurrent?.path, runAnalysis, runAutodetect])

const setSequences = useCallback(
(inputs: AlgorithmInput[]) => {
addQryInputs(inputs)

if (shouldRunAutomatically) {
run()
}
Expand All @@ -62,7 +71,6 @@ export function MainInputFormSequenceFilePicker() {
const setExampleSequences = useCallback(() => {
if (datasetCurrent) {
addQryInputs([new AlgorithmInputDefault(datasetCurrent)])

if (shouldRunAutomatically) {
run()
}
Expand All @@ -81,7 +89,7 @@ export function MainInputFormSequenceFilePicker() {
}, [canRun, hasInputErrors, hasRequiredInputs, t])

const LoadExampleLink = useMemo(() => {
const cannotLoadExample = hasInputErrors || !datasetCurrent
const cannotLoadExample = hasInputErrors || !datasetCurrent || datasetCurrent.path === 'autodetect'
return (
<Button color="link" onClick={setExampleSequences} disabled={cannotLoadExample}>
{t('Load example')}
Expand Down
58 changes: 58 additions & 0 deletions packages_rs/nextclade-web/src/hooks/useRunSeqAutodetect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { useRouter } from 'next/router'
import { NextcladeSeqAutodetectWasm } from 'src/gen/nextclade-wasm'
import { useRecoilCallback } from 'recoil'
import { ErrorInternal } from 'src/helpers/ErrorInternal'
import { axiosFetch } from 'src/io/axiosFetch'
import { autodetectResultAtom, autodetectResultsAtom, minimizerIndexAtom } from 'src/state/autodetect.state'
import { minimizerIndexVersionAtom } from 'src/state/dataset.state'
import { MinimizerIndexJson, MinimizerSearchRecord } from 'src/types'
import { qrySeqInputsStorageAtom } from 'src/state/inputs.state'
import { getQueryFasta } from 'src/workers/launchAnalysis'

export function useRunSeqAutodetect() {
const router = useRouter()

return useRecoilCallback(
({ set, reset, snapshot: { getPromise } }) =>
() => {
reset(minimizerIndexAtom)
reset(autodetectResultsAtom)

void router.push('/autodetect', '/autodetect') // eslint-disable-line no-void

function onResult(res: MinimizerSearchRecord) {
set(autodetectResultAtom(res.fastaRecord.index), res)
}

Promise.all([getPromise(qrySeqInputsStorageAtom), getPromise(minimizerIndexVersionAtom)])
.then(async ([qrySeqInputs, minimizerIndexVersion]) => {
if (!minimizerIndexVersion) {
throw new ErrorInternal('Tried to run minimizer search without minimizer index available')
}
const fasta = await getQueryFasta(qrySeqInputs)
const minimizerIndex: MinimizerIndexJson = await axiosFetch(minimizerIndexVersion.path)
set(minimizerIndexAtom, minimizerIndex)
return runAutodetect(fasta, minimizerIndex, onResult)
})
.catch((error) => {
throw error
})
},
[router],
)
}

function runAutodetect(
fasta: string,
minimizerIndex: MinimizerIndexJson,
onResult: (res: MinimizerSearchRecord) => void,
) {
const nextcladeAutodetect = NextcladeSeqAutodetectWasm.new(JSON.stringify(minimizerIndex))

function onResultParsed(resStr: string) {
const result = JSON.parse(resStr) as MinimizerSearchRecord
onResult(result)
}

nextcladeAutodetect.autodetect(fasta, onResultParsed)
}
Loading

0 comments on commit 8ac3f67

Please sign in to comment.