@jbrowse/plugin-linear-genome-view 1.4.1 → 1.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/BaseLinearDisplay/components/Block.d.ts +7 -10
- package/dist/BaseLinearDisplay/models/BaseLinearDisplayModel.d.ts +16 -9
- package/dist/BaseLinearDisplay/models/serverSideRenderedBlock.d.ts +2 -2
- package/dist/LinearBareDisplay/model.d.ts +8 -8
- package/dist/LinearBasicDisplay/model.d.ts +11 -8
- package/dist/LinearGenomeView/components/HelpDialog.d.ts +5 -0
- package/dist/LinearGenomeView/components/LinearGenomeView.d.ts +3 -5
- package/dist/LinearGenomeView/components/LinearGenomeViewSvg.d.ts +4 -0
- package/dist/LinearGenomeView/components/OverviewRubberBand.d.ts +2 -3
- package/dist/LinearGenomeView/components/OverviewScaleBar.d.ts +116 -2
- package/dist/LinearGenomeView/components/RefNameAutocomplete.d.ts +3 -11
- package/dist/LinearGenomeView/components/ScaleBar.d.ts +36 -2
- package/dist/LinearGenomeView/components/util.d.ts +2 -0
- package/dist/LinearGenomeView/index.d.ts +22 -4
- package/dist/index.d.ts +26 -26
- package/dist/plugin-linear-genome-view.cjs.development.js +3178 -2884
- package/dist/plugin-linear-genome-view.cjs.development.js.map +1 -1
- package/dist/plugin-linear-genome-view.cjs.production.min.js +1 -1
- package/dist/plugin-linear-genome-view.cjs.production.min.js.map +1 -1
- package/dist/plugin-linear-genome-view.esm.js +3191 -2898
- package/dist/plugin-linear-genome-view.esm.js.map +1 -1
- package/package.json +2 -2
- package/src/BaseLinearDisplay/components/BaseLinearDisplay.tsx +3 -0
- package/src/BaseLinearDisplay/components/Block.tsx +20 -33
- package/src/BaseLinearDisplay/models/BaseLinearDisplayModel.tsx +3 -7
- package/src/BaseLinearDisplay/models/serverSideRenderedBlock.ts +15 -13
- package/src/LinearBasicDisplay/model.ts +25 -3
- package/src/LinearGenomeView/components/ExportSvgDialog.tsx +6 -6
- package/src/LinearGenomeView/components/Header.tsx +56 -78
- package/src/LinearGenomeView/components/HelpDialog.tsx +81 -0
- package/src/LinearGenomeView/components/ImportForm.tsx +139 -158
- package/src/LinearGenomeView/components/LinearGenomeView.test.js +6 -6
- package/src/LinearGenomeView/components/LinearGenomeView.tsx +30 -245
- package/src/LinearGenomeView/components/LinearGenomeViewSvg.tsx +317 -0
- package/src/LinearGenomeView/components/OverviewRubberBand.tsx +74 -34
- package/src/LinearGenomeView/components/OverviewScaleBar.tsx +326 -177
- package/src/LinearGenomeView/components/RefNameAutocomplete.tsx +152 -157
- package/src/LinearGenomeView/components/SearchResultsDialog.tsx +12 -34
- package/src/LinearGenomeView/components/SequenceDialog.tsx +10 -9
- package/src/LinearGenomeView/components/__snapshots__/LinearGenomeView.test.js.snap +127 -254
- package/src/LinearGenomeView/components/util.ts +10 -0
- package/src/LinearGenomeView/index.tsx +69 -27
- package/src/index.ts +3 -1
|
@@ -1,20 +1,23 @@
|
|
|
1
|
-
import React, { useState } from 'react'
|
|
1
|
+
import React, { useState, lazy } from 'react'
|
|
2
2
|
import { observer } from 'mobx-react'
|
|
3
3
|
import { getSession } from '@jbrowse/core/util'
|
|
4
|
-
// import BaseResult from '@jbrowse/core/TextSearch/BaseResults'
|
|
5
|
-
import AssemblySelector from '@jbrowse/core/ui/AssemblySelector'
|
|
6
4
|
import {
|
|
7
5
|
Button,
|
|
8
6
|
CircularProgress,
|
|
9
7
|
Container,
|
|
10
8
|
Grid,
|
|
11
|
-
Typography,
|
|
12
9
|
makeStyles,
|
|
13
10
|
} from '@material-ui/core'
|
|
14
|
-
|
|
11
|
+
import { SearchType } from '@jbrowse/core/data_adapters/BaseAdapter'
|
|
12
|
+
import BaseResult from '@jbrowse/core/TextSearch/BaseResults'
|
|
13
|
+
import AssemblySelector from '@jbrowse/core/ui/AssemblySelector'
|
|
14
|
+
import ErrorMessage from '@jbrowse/core/ui/ErrorMessage'
|
|
15
|
+
import CloseIcon from '@material-ui/icons/Close'
|
|
16
|
+
|
|
17
|
+
// locals
|
|
15
18
|
import RefNameAutocomplete from './RefNameAutocomplete'
|
|
16
|
-
import SearchResultsDialog from './SearchResultsDialog'
|
|
17
19
|
import { LinearGenomeViewModel } from '..'
|
|
20
|
+
const SearchResultsDialog = lazy(() => import('./SearchResultsDialog'))
|
|
18
21
|
|
|
19
22
|
const useStyles = makeStyles(theme => ({
|
|
20
23
|
importFormContainer: {
|
|
@@ -27,14 +30,6 @@ const useStyles = makeStyles(theme => ({
|
|
|
27
30
|
|
|
28
31
|
type LGV = LinearGenomeViewModel
|
|
29
32
|
|
|
30
|
-
const ErrorDisplay = observer(({ error }: { error?: Error | string }) => {
|
|
31
|
-
return (
|
|
32
|
-
<Typography variant="h6" color="error">
|
|
33
|
-
{`${error}`}
|
|
34
|
-
</Typography>
|
|
35
|
-
)
|
|
36
|
-
})
|
|
37
|
-
|
|
38
33
|
const ImportForm = observer(({ model }: { model: LGV }) => {
|
|
39
34
|
const classes = useStyles()
|
|
40
35
|
const session = getSession(model)
|
|
@@ -46,7 +41,6 @@ const ImportForm = observer(({ model }: { model: LGV }) => {
|
|
|
46
41
|
} = model
|
|
47
42
|
const [selectedAsm, setSelectedAsm] = useState(assemblyNames[0])
|
|
48
43
|
const [error, setError] = useState<typeof modelError | undefined>(modelError)
|
|
49
|
-
const message = !assemblyNames.length ? 'No configured assemblies' : ''
|
|
50
44
|
const searchScope = model.searchScope(selectedAsm)
|
|
51
45
|
|
|
52
46
|
const assembly = assemblyManager.get(selectedAsm)
|
|
@@ -55,179 +49,166 @@ const ImportForm = observer(({ model }: { model: LGV }) => {
|
|
|
55
49
|
: 'No configured assemblies'
|
|
56
50
|
const regions = assembly?.regions || []
|
|
57
51
|
const err = assemblyError || error
|
|
58
|
-
const [mySelectedRegion, setSelectedRegion] = useState<string>()
|
|
59
|
-
const [optionTrackId, setOptionTrackId] = useState<string>()
|
|
60
|
-
const [optionLocation, setOptionLocation] = useState<string>()
|
|
61
|
-
const selectedRegion = mySelectedRegion || regions[0]?.refName
|
|
62
52
|
|
|
63
|
-
|
|
53
|
+
const [myOption, setOption] = useState<BaseResult>()
|
|
54
|
+
|
|
55
|
+
// use this instead of useState initializer because the useState initializer
|
|
56
|
+
// won't update in response to an observable
|
|
57
|
+
const option =
|
|
58
|
+
myOption ||
|
|
59
|
+
new BaseResult({
|
|
60
|
+
label: regions[0]?.refName,
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
const selectedRegion = option?.getLocation()
|
|
64
|
+
|
|
65
|
+
async function fetchResults(query: string, searchType?: SearchType) {
|
|
64
66
|
if (!textSearchManager) {
|
|
65
67
|
console.warn('No text search manager')
|
|
66
68
|
}
|
|
67
|
-
|
|
69
|
+
|
|
70
|
+
const textSearchResults = await textSearchManager?.search(
|
|
68
71
|
{
|
|
69
|
-
queryString:
|
|
70
|
-
searchType
|
|
72
|
+
queryString: query,
|
|
73
|
+
searchType,
|
|
71
74
|
},
|
|
72
75
|
searchScope,
|
|
73
76
|
rankSearchResults,
|
|
74
77
|
)
|
|
75
78
|
|
|
76
|
-
|
|
77
|
-
(
|
|
78
|
-
|
|
79
|
-
|
|
79
|
+
const refNameResults = assembly?.allRefNames
|
|
80
|
+
?.filter(refName => refName.startsWith(query))
|
|
81
|
+
.map(r => new BaseResult({ label: r }))
|
|
82
|
+
.slice(0, 10)
|
|
83
|
+
|
|
84
|
+
return [...(refNameResults || []), ...(textSearchResults || [])]
|
|
80
85
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
* textSearchManager
|
|
88
|
-
* 3) if we get any hits by requerying the textSearchManager, then we either
|
|
89
|
-
* navigate to single hit's location or pop open the the dialog with all the results
|
|
90
|
-
* 4) if there were no hits from requerying, then we use (1) the chosen options'
|
|
91
|
-
* trackId and locStr to navigate and show that track
|
|
92
|
-
* 5) error handling
|
|
93
|
-
* @param input - selectedRegion/result label
|
|
94
|
-
*/
|
|
86
|
+
|
|
87
|
+
// gets a string as input, or use stored option results from previous query,
|
|
88
|
+
// then re-query and
|
|
89
|
+
// 1) if it has multiple results: pop a dialog
|
|
90
|
+
// 2) if it's a single result navigate to it
|
|
91
|
+
// 3) else assume it's a locstring and navigate to it
|
|
95
92
|
async function handleSelectedRegion(input: string) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
} else {
|
|
105
|
-
const results = await fetchResults(input)
|
|
106
|
-
if (results && results.length > 1) {
|
|
107
|
-
model.setSearchResults(results, input.toLowerCase())
|
|
93
|
+
if (!option) {
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
let trackId = option.getTrackId()
|
|
97
|
+
let location = input || option.getLocation() || ''
|
|
98
|
+
try {
|
|
99
|
+
if (assembly?.allRefNames?.includes(location)) {
|
|
100
|
+
model.navToLocString(location, selectedAsm)
|
|
108
101
|
} else {
|
|
109
|
-
|
|
102
|
+
const results = await fetchResults(input, 'exact')
|
|
103
|
+
if (results && results.length > 1) {
|
|
104
|
+
model.setSearchResults(results, input.toLowerCase())
|
|
105
|
+
return
|
|
106
|
+
} else if (results?.length === 1) {
|
|
110
107
|
location = results[0].getLocation()
|
|
111
108
|
trackId = results[0].getTrackId()
|
|
112
109
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
} catch (e) {
|
|
118
|
-
if (`${e}` === `Error: Unknown reference sequence "${input}"`) {
|
|
119
|
-
model.setSearchResults(results, input.toLocaleLowerCase())
|
|
120
|
-
} else {
|
|
121
|
-
console.warn(e)
|
|
122
|
-
session.notify(`${e}`, 'warning')
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
try {
|
|
126
|
-
if (trackId) {
|
|
127
|
-
model.showTrack(trackId)
|
|
128
|
-
}
|
|
129
|
-
} catch (e) {
|
|
130
|
-
console.warn(
|
|
131
|
-
`'${e}' occurred while attempting to show track: ${trackId}`,
|
|
132
|
-
)
|
|
110
|
+
|
|
111
|
+
model.navToLocString(location, selectedAsm)
|
|
112
|
+
if (trackId) {
|
|
113
|
+
model.showTrack(trackId)
|
|
133
114
|
}
|
|
134
115
|
}
|
|
116
|
+
} catch (e) {
|
|
117
|
+
console.error(e)
|
|
118
|
+
session.notify(`${e}`, 'warning')
|
|
135
119
|
}
|
|
136
120
|
}
|
|
137
121
|
|
|
122
|
+
// implementation notes:
|
|
123
|
+
// having this wrapped in a form allows intuitive use of enter key to submit
|
|
138
124
|
return (
|
|
139
125
|
<div>
|
|
140
|
-
{err ? <
|
|
126
|
+
{err ? <ErrorMessage error={err} /> : null}
|
|
141
127
|
<Container className={classes.importFormContainer}>
|
|
142
|
-
<
|
|
143
|
-
<Grid
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
>
|
|
219
|
-
Show all regions in assembly
|
|
220
|
-
</Button>
|
|
128
|
+
<form onSubmit={event => event.preventDefault()}>
|
|
129
|
+
<Grid
|
|
130
|
+
container
|
|
131
|
+
spacing={1}
|
|
132
|
+
justifyContent="center"
|
|
133
|
+
alignItems="center"
|
|
134
|
+
>
|
|
135
|
+
<Grid item>
|
|
136
|
+
<AssemblySelector
|
|
137
|
+
onChange={val => {
|
|
138
|
+
setError(undefined)
|
|
139
|
+
setSelectedAsm(val)
|
|
140
|
+
}}
|
|
141
|
+
session={session}
|
|
142
|
+
selected={selectedAsm}
|
|
143
|
+
/>
|
|
144
|
+
</Grid>
|
|
145
|
+
<Grid item>
|
|
146
|
+
{selectedAsm ? (
|
|
147
|
+
err ? (
|
|
148
|
+
<CloseIcon style={{ color: 'red' }} />
|
|
149
|
+
) : selectedRegion ? (
|
|
150
|
+
<RefNameAutocomplete
|
|
151
|
+
fetchResults={fetchResults}
|
|
152
|
+
model={model}
|
|
153
|
+
assemblyName={assemblyError ? undefined : selectedAsm}
|
|
154
|
+
value={selectedRegion}
|
|
155
|
+
// note: minWidth 270 accomodates full width of helperText
|
|
156
|
+
minWidth={270}
|
|
157
|
+
onSelect={option => setOption(option)}
|
|
158
|
+
TextFieldProps={{
|
|
159
|
+
margin: 'normal',
|
|
160
|
+
variant: 'outlined',
|
|
161
|
+
helperText:
|
|
162
|
+
'Enter sequence name, feature name, or location',
|
|
163
|
+
}}
|
|
164
|
+
/>
|
|
165
|
+
) : (
|
|
166
|
+
<CircularProgress
|
|
167
|
+
role="progressbar"
|
|
168
|
+
size={20}
|
|
169
|
+
disableShrink
|
|
170
|
+
/>
|
|
171
|
+
)
|
|
172
|
+
) : null}
|
|
173
|
+
</Grid>
|
|
174
|
+
<Grid item></Grid>
|
|
175
|
+
<Grid item>
|
|
176
|
+
<Button
|
|
177
|
+
type="submit"
|
|
178
|
+
disabled={!selectedRegion}
|
|
179
|
+
className={classes.button}
|
|
180
|
+
onClick={() => {
|
|
181
|
+
model.setError(undefined)
|
|
182
|
+
if (selectedRegion) {
|
|
183
|
+
handleSelectedRegion(selectedRegion)
|
|
184
|
+
}
|
|
185
|
+
}}
|
|
186
|
+
variant="contained"
|
|
187
|
+
color="primary"
|
|
188
|
+
>
|
|
189
|
+
Open
|
|
190
|
+
</Button>
|
|
191
|
+
<Button
|
|
192
|
+
disabled={!selectedRegion}
|
|
193
|
+
className={classes.button}
|
|
194
|
+
onClick={() => {
|
|
195
|
+
model.setError(undefined)
|
|
196
|
+
model.showAllRegionsInAssembly(selectedAsm)
|
|
197
|
+
}}
|
|
198
|
+
variant="contained"
|
|
199
|
+
color="secondary"
|
|
200
|
+
>
|
|
201
|
+
Show all regions in assembly
|
|
202
|
+
</Button>
|
|
203
|
+
</Grid>
|
|
221
204
|
</Grid>
|
|
222
|
-
</
|
|
205
|
+
</form>
|
|
223
206
|
</Container>
|
|
224
207
|
{isSearchDialogDisplayed ? (
|
|
225
208
|
<SearchResultsDialog
|
|
226
209
|
model={model}
|
|
227
210
|
optAssemblyName={selectedAsm}
|
|
228
|
-
handleClose={() =>
|
|
229
|
-
model.setSearchResults(undefined, undefined)
|
|
230
|
-
}}
|
|
211
|
+
handleClose={() => model.setSearchResults(undefined, undefined)}
|
|
231
212
|
/>
|
|
232
213
|
) : null}
|
|
233
214
|
</div>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
|
-
import { render } from '@testing-library/react'
|
|
2
|
+
import { fireEvent, render, waitFor } from '@testing-library/react'
|
|
3
3
|
import { createTestSession } from '@jbrowse/web/src/rootModel'
|
|
4
4
|
import sizeMe from 'react-sizeme'
|
|
5
5
|
import 'requestidlecallback-polyfill'
|
|
@@ -34,12 +34,12 @@ describe('<LinearGenomeView />', () => {
|
|
|
34
34
|
session.addView('LinearGenomeView', { id: 'lgv' })
|
|
35
35
|
const model = session.views[0]
|
|
36
36
|
model.setWidth(800)
|
|
37
|
-
const {
|
|
38
|
-
const openButton = await findByText('Open')
|
|
39
|
-
expect(container.firstChild).toMatchSnapshot()
|
|
37
|
+
const { findByText } = render(<LinearGenomeView model={model} />)
|
|
40
38
|
expect(model.displayedRegions.length).toEqual(0)
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
fireEvent.click(await findByText('Open'))
|
|
40
|
+
await waitFor(() => {
|
|
41
|
+
expect(model.displayedRegions.length).toEqual(1)
|
|
42
|
+
})
|
|
43
43
|
})
|
|
44
44
|
it('renders one track, one region', async () => {
|
|
45
45
|
const session = createTestSession()
|