Skip to content

Commit

Permalink
Allow searching by gene name using linear synteny view (#4096)
Browse files Browse the repository at this point in the history
  • Loading branch information
cmdcolin authored Nov 29, 2023
1 parent ab31551 commit 87bbb99
Show file tree
Hide file tree
Showing 6 changed files with 147 additions and 118 deletions.
11 changes: 8 additions & 3 deletions plugins/linear-comparative-view/src/LaunchLinearSyntenyView.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import PluginManager from '@jbrowse/core/PluginManager'
import { AbstractSessionModel } from '@jbrowse/core/util'
import { LinearSyntenyViewModel } from './LinearSyntenyView/model'
import { when } from 'mobx'

// locals
import { LinearSyntenyViewModel } from './LinearSyntenyView/model'
type LSV = LinearSyntenyViewModel

export default function LaunchLinearSyntenyView(pluginManager: PluginManager) {
Expand Down Expand Up @@ -48,8 +49,12 @@ export default function LaunchLinearSyntenyView(pluginManager: PluginManager) {
await Promise.all(
views.map(async (data, idx) => {
const view = model.views[idx]
const { loc, tracks = [] } = data
await view.navToLocString(loc)
const { assembly, loc, tracks = [] } = data
const asm = await assemblyManager.waitForAssembly(assembly)
if (!asm) {
throw new Error(`Assembly ${data.assembly} failed to load`)
}
await view.navToSearchString({ input: loc, assembly: asm })
tracks.forEach(track => tryTrack(view, track, idsNotFound))
}),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ import BaseResult from '@jbrowse/core/TextSearch/BaseResults'
import CloseIcon from '@mui/icons-material/Close'

// locals
import RefNameAutocomplete from './RefNameAutocomplete'
import { fetchResults, splitLast } from './util'
import { LinearGenomeViewModel } from '..'
import { handleSelectedRegion, navToOption } from '../../searchUtils'
import ImportFormRefNameAutocomplete from './ImportFormRefNameAutocomplete'

const useStyles = makeStyles()(theme => ({
importFormContainer: {
Expand All @@ -41,11 +41,10 @@ const LinearGenomeViewImportForm = observer(function ({
}) {
const { classes } = useStyles()
const session = getSession(model)
const { assemblyNames, assemblyManager, textSearchManager } = session
const { rankSearchResults, error } = model
const { assemblyNames, assemblyManager } = session
const { error } = model
const [selectedAsm, setSelectedAsm] = useState(assemblyNames[0])
const [option, setOption] = useState<BaseResult>()
const searchScope = model.searchScope(selectedAsm)
const assembly = assemblyManager.get(selectedAsm)
const assemblyError = assemblyNames.length
? assembly?.error
Expand All @@ -65,61 +64,6 @@ const LinearGenomeViewImportForm = observer(function ({
setValue(r0)
}, [r0, selectedAsm])

async function navToOption(option: BaseResult) {
const location = option.getLocation()
const trackId = option.getTrackId()
if (location) {
await model.navToLocString(location, selectedAsm)
if (trackId) {
model.showTrack(trackId)
}
}
}

// gets a string as input, or use stored option results from previous query,
// then re-query and
// 1) if it has multiple results: pop a dialog
// 2) if it's a single result navigate to it
// 3) else assume it's a locstring and navigate to it
async function handleSelectedRegion(input: string) {
try {
if (option?.getDisplayString() === input && option.hasLocation()) {
await navToOption(option)
} else if (option?.results?.length) {
model.setSearchResults(option.results, option.getLabel(), selectedAsm)
} else {
const [ref, rest] = splitLast(input, ':')
const allRefs = assembly?.allRefNamesWithLowerCase || []
if (
allRefs.includes(input) ||
(allRefs.includes(ref) && !Number.isNaN(Number.parseInt(rest, 10)))
) {
await model.navToLocString(input, selectedAsm)
} else {
const results = await fetchResults({
queryString: input,
searchType: 'exact',
searchScope,
rankSearchResults,
textSearchManager,
assembly,
})

if (results.length > 1) {
model.setSearchResults(results, input.toLowerCase(), selectedAsm)
} else if (results.length === 1) {
await navToOption(results[0])
} else {
await model.navToLocString(input, selectedAsm)
}
}
}
} catch (e) {
console.error(e)
session.notify(`${e}`, 'warning')
}
}

// implementation notes:
// having this wrapped in a form allows intuitive use of enter key to submit
return (
Expand All @@ -132,7 +76,29 @@ const LinearGenomeViewImportForm = observer(function ({
model.setError(undefined)
if (value) {
// has it's own error handling
await handleSelectedRegion(value)
try {
if (
option?.getDisplayString() === value &&
option.hasLocation()
) {
await navToOption({
option,
model,
assemblyName: selectedAsm,
})
} else if (option?.results?.length) {
model.setSearchResults(
option.results,
option.getLabel(),
selectedAsm,
)
} else if (assembly) {
await handleSelectedRegion({ input: value, assembly, model })
}
} catch (e) {
console.error(e)
session.notify(`${e}`, 'warning')
}
}
}}
>
Expand All @@ -158,27 +124,12 @@ const LinearGenomeViewImportForm = observer(function ({
<CloseIcon style={{ color: 'red' }} />
) : assemblyLoaded ? (
<FormControl>
<RefNameAutocomplete
fetchResults={queryString =>
fetchResults({
queryString,
assembly,
textSearchManager,
rankSearchResults,
searchScope,
})
}
model={model}
assemblyName={selectedAsm}
<ImportFormRefNameAutocomplete
value={value}
minWidth={270}
onChange={str => setValue(str)}
onSelect={val => setOption(val)}
TextFieldProps={{
variant: 'outlined',
helperText:
'Enter sequence name, feature name, or location',
}}
setValue={setValue}
selectedAsm={selectedAsm}
setOption={setOption}
model={model}
/>
</FormControl>
) : (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import React from 'react'
import { observer } from 'mobx-react'
import { getSession } from '@jbrowse/core/util'
import BaseResult from '@jbrowse/core/TextSearch/BaseResults'

// locals
import RefNameAutocomplete from './RefNameAutocomplete'
import { fetchResults } from './util'
import { LinearGenomeViewModel } from '..'

type LGV = LinearGenomeViewModel

const ImportFormRefNameAutocomplete = observer(function ({
model,
selectedAsm,
value,
setValue,
setOption,
}: {
value: string
setValue: (arg: string) => void
model: LGV
selectedAsm: string
setOption: (arg: BaseResult) => void
}) {
const session = getSession(model)
const { assemblyManager, textSearchManager } = session
const { rankSearchResults } = model
const searchScope = model.searchScope(selectedAsm)
const assembly = assemblyManager.get(selectedAsm)
return (
<RefNameAutocomplete
fetchResults={queryString =>
fetchResults({
queryString,
assembly,
textSearchManager,
rankSearchResults,
searchScope,
})
}
model={model}
assemblyName={selectedAsm}
value={value}
minWidth={270}
onChange={str => setValue(str)}
onSelect={val => setOption(val)}
TextFieldProps={{
variant: 'outlined',
helperText: 'Enter sequence name, feature name, or location',
}}
/>
)
})

export default ImportFormRefNameAutocomplete
22 changes: 22 additions & 0 deletions plugins/linear-genome-view/src/LinearGenomeView/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ import MenuOpenIcon from '@mui/icons-material/MenuOpen'
import MiniControls from './components/MiniControls'
import Header from './components/Header'
import { generateLocations, parseLocStrings } from './util'
import { Assembly } from '@jbrowse/core/assemblyManager/assembly'
import { handleSelectedRegion } from '../searchUtils'
// lazies
const ReturnToImportFormDialog = lazy(
() => import('@jbrowse/core/ui/ReturnToImportFormDialog'),
Expand Down Expand Up @@ -1315,6 +1317,26 @@ export function stateModelFactory(pluginManager: PluginManager) {
)
},

/**
* #action
* Performs a text index search, and navigates to it immediately if a
* single result is returned. Will pop up a search dialog if multiple
* results are returned
*/
async navToSearchString({
input,
assembly,
}: {
input: string
assembly: Assembly
}) {
await handleSelectedRegion({
input,
assembly,
model: self as LinearGenomeViewModel,
})
},

/**
* #action
* Similar to `navToLocString`, but accepts parsed location objects
Expand Down
55 changes: 25 additions & 30 deletions plugins/linear-genome-view/src/searchUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,38 +41,33 @@ export async function handleSelectedRegion({
model: LinearGenomeViewModel
assembly: Assembly
}) {
try {
const allRefs = assembly?.allRefNamesWithLowerCase || []
const assemblyName = assembly.name
if (input.split(' ').every(entry => checkRef(entry, allRefs))) {
await model.navToLocString(input, assembly.name)
} else {
const searchScope = model.searchScope(assemblyName)
const { textSearchManager } = getSession(model)
const results = await fetchResults({
queryString: input,
searchType: 'exact',
searchScope,
rankSearchResults: model.rankSearchResults,
textSearchManager,
assembly,
})
const allRefs = assembly?.allRefNamesWithLowerCase || []
const assemblyName = assembly.name
if (input.split(' ').every(entry => checkRef(entry, allRefs))) {
await model.navToLocString(input, assembly.name)
} else {
const searchScope = model.searchScope(assemblyName)
const { textSearchManager } = getSession(model)
const results = await fetchResults({
queryString: input,
searchType: 'exact',
searchScope,
rankSearchResults: model.rankSearchResults,
textSearchManager,
assembly,
})

if (results.length > 1) {
model.setSearchResults(results, input.toLowerCase(), assemblyName)
} else if (results.length === 1) {
await navToOption({
option: results[0],
model,
assemblyName,
})
} else {
await model.navToLocString(input, assemblyName)
}
if (results.length > 1) {
model.setSearchResults(results, input.toLowerCase(), assemblyName)
} else if (results.length === 1) {
await navToOption({
option: results[0],
model,
assemblyName,
})
} else {
await model.navToLocString(input, assemblyName)
}
} catch (e) {
console.error(e)
getSession(model).notify(`${e}`, 'warning')
}
}

Expand Down
8 changes: 4 additions & 4 deletions products/jbrowse-web/src/tests/JBrowse.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jest.mock('../makeWorkerInstance', () => () => {})

setup()

const delay = { timeout: 15000 }
const delay = { timeout: 30000 }

beforeEach(() => {
doBeforeEach()
Expand Down Expand Up @@ -70,7 +70,7 @@ test('assembly aliases', async () => {
await findByTestId(hts('volvox_filtered_vcf_assembly_alias'), {}, delay),
)
await findByTestId('box-test-vcf-604453', {}, delay)
}, 15000)
}, 30000)

test('nclist track test with long name', async () => {
const { view, findByTestId, findByText } = await createView()
Expand Down Expand Up @@ -98,7 +98,7 @@ test('test sharing', async () => {
expect(
((await findByLabelText('URL', {}, delay)) as HTMLInputElement).value,
).toBe('http://localhost/?session=share-abc&password=123')
}, 15000)
}, 30000)

test('looks at about this track dialog', async () => {
const { findByTestId, findAllByText, findByText } = await createView()
Expand All @@ -108,4 +108,4 @@ test('looks at about this track dialog', async () => {
fireEvent.click(await findByTestId('track_menu_icon', {}, delay))
fireEvent.click(await findByText('About track'))
await findAllByText(/SQ/, {}, delay)
}, 15000)
}, 30000)

0 comments on commit 87bbb99

Please sign in to comment.