@jbrowse/plugin-linear-genome-view 1.5.0 → 1.5.4

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.
Files changed (33) hide show
  1. package/dist/BaseLinearDisplay/components/Block.d.ts +7 -10
  2. package/dist/LinearGenomeView/components/HelpDialog.d.ts +5 -0
  3. package/dist/LinearGenomeView/components/LinearGenomeView.d.ts +3 -5
  4. package/dist/LinearGenomeView/components/LinearGenomeViewSvg.d.ts +4 -0
  5. package/dist/LinearGenomeView/components/OverviewRubberBand.d.ts +2 -3
  6. package/dist/LinearGenomeView/components/OverviewScaleBar.d.ts +116 -2
  7. package/dist/LinearGenomeView/components/RefNameAutocomplete.d.ts +3 -1
  8. package/dist/LinearGenomeView/components/ScaleBar.d.ts +14 -0
  9. package/dist/LinearGenomeView/components/util.d.ts +1 -1
  10. package/dist/LinearGenomeView/index.d.ts +9 -2
  11. package/dist/plugin-linear-genome-view.cjs.development.js +3163 -2886
  12. package/dist/plugin-linear-genome-view.cjs.development.js.map +1 -1
  13. package/dist/plugin-linear-genome-view.cjs.production.min.js +1 -1
  14. package/dist/plugin-linear-genome-view.cjs.production.min.js.map +1 -1
  15. package/dist/plugin-linear-genome-view.esm.js +3122 -2845
  16. package/dist/plugin-linear-genome-view.esm.js.map +1 -1
  17. package/package.json +4 -4
  18. package/src/BaseLinearDisplay/components/BaseLinearDisplay.tsx +0 -1
  19. package/src/BaseLinearDisplay/components/Block.tsx +20 -33
  20. package/src/LinearGenomeView/components/Header.tsx +19 -7
  21. package/src/LinearGenomeView/components/HelpDialog.tsx +81 -0
  22. package/src/LinearGenomeView/components/ImportForm.tsx +42 -51
  23. package/src/LinearGenomeView/components/LinearGenomeView.tsx +30 -245
  24. package/src/LinearGenomeView/components/LinearGenomeViewSvg.tsx +317 -0
  25. package/src/LinearGenomeView/components/OverviewRubberBand.tsx +74 -34
  26. package/src/LinearGenomeView/components/OverviewScaleBar.tsx +326 -177
  27. package/src/LinearGenomeView/components/RefNameAutocomplete.tsx +145 -145
  28. package/src/LinearGenomeView/components/SearchResultsDialog.tsx +12 -34
  29. package/src/LinearGenomeView/components/SequenceDialog.tsx +9 -8
  30. package/src/LinearGenomeView/components/__snapshots__/LinearGenomeView.test.js.snap +127 -78
  31. package/src/LinearGenomeView/components/util.ts +6 -4
  32. package/src/LinearGenomeView/index.tsx +55 -14
  33. package/src/declare.d.ts +0 -1
@@ -1,58 +1,37 @@
1
- import React, { useMemo, useEffect, useState } from 'react'
1
+ import React, { lazy, useMemo, useEffect, useState } from 'react'
2
2
  import { observer } from 'mobx-react'
3
-
4
- // jbrowse core
5
3
  import { getSession, useDebounce, measureText } from '@jbrowse/core/util'
6
4
  import BaseResult, {
7
5
  RefSequenceResult,
8
6
  } from '@jbrowse/core/TextSearch/BaseResults'
9
-
10
- // material ui
11
7
  import {
12
8
  CircularProgress,
9
+ IconButton,
13
10
  InputAdornment,
14
11
  Popper,
12
+ PopperProps,
15
13
  TextField,
16
14
  TextFieldProps as TFP,
17
- PopperProps,
18
15
  Typography,
19
16
  } from '@material-ui/core'
17
+
18
+ // icons
20
19
  import SearchIcon from '@material-ui/icons/Search'
21
20
  import Autocomplete from '@material-ui/lab/Autocomplete'
21
+ import HelpIcon from '@material-ui/icons/Help'
22
22
 
23
23
  // locals
24
24
  import { LinearGenomeViewModel } from '..'
25
+ import { dedupe } from './util'
26
+
27
+ // lazy
28
+ const HelpDialog = lazy(() => import('./HelpDialog'))
25
29
 
26
30
  export interface Option {
27
31
  group?: string
28
32
  result: BaseResult
29
33
  }
30
34
 
31
- async function fetchResults(
32
- self: LinearGenomeViewModel,
33
- query: string,
34
- assemblyName: string,
35
- ) {
36
- const { textSearchManager } = getSession(self)
37
- const { rankSearchResults } = self
38
- const searchScope = self.searchScope(assemblyName)
39
- return textSearchManager
40
- ?.search(
41
- {
42
- queryString: query,
43
- searchType: 'prefix',
44
- },
45
- searchScope,
46
- rankSearchResults,
47
- )
48
- .then(results =>
49
- results.filter(
50
- (elem, index, self) =>
51
- index === self.findIndex(t => t.label === elem.label),
52
- ),
53
- )
54
- }
55
-
56
35
  // the logic of this method is to only apply a filter to RefSequenceResults
57
36
  // because they do not have a matchedObject. the trix search results already
58
37
  // filter so don't need re-filtering
@@ -90,23 +69,28 @@ function RefNameAutocomplete({
90
69
  onSelect,
91
70
  assemblyName,
92
71
  style,
72
+ fetchResults,
93
73
  value,
74
+ minWidth = 200,
94
75
  TextFieldProps = {},
95
76
  }: {
96
77
  model: LinearGenomeViewModel
97
78
  onSelect: (region: BaseResult) => void
98
79
  assemblyName?: string
99
80
  value?: string
81
+ fetchResults: (query: string) => Promise<BaseResult[]>
100
82
  style?: React.CSSProperties
83
+ minWidth?: number
101
84
  TextFieldProps?: TFP
102
85
  }) {
103
86
  const session = getSession(model)
104
87
  const { assemblyManager } = session
105
88
  const [open, setOpen] = useState(false)
106
89
  const [loaded, setLoaded] = useState(true)
90
+ const [isHelpDialogDisplayed, setHelpDialogDisplayed] = useState(false)
107
91
  const [currentSearch, setCurrentSearch] = useState('')
108
92
  const [inputValue, setInputValue] = useState('')
109
- const [searchOptions, setSearchOptions] = useState([] as Option[])
93
+ const [searchOptions, setSearchOptions] = useState<Option[]>()
110
94
  const debouncedSearch = useDebounce(currentSearch, 300)
111
95
  const { coarseVisibleLocStrings, hasDisplayedRegions } = model
112
96
  const assembly = assemblyName ? assemblyManager.get(assemblyName) : undefined
@@ -136,11 +120,13 @@ function RefNameAutocomplete({
136
120
  }
137
121
 
138
122
  setLoaded(false)
139
- const results = await fetchResults(model, debouncedSearch, assemblyName)
123
+ const results = await fetchResults(debouncedSearch)
140
124
  if (active) {
141
- if (results && results.length >= 0) {
142
- setSearchOptions(results.map(result => ({ result })))
143
- }
125
+ setSearchOptions(
126
+ dedupe(results, r => r.getDisplayString()).map(result => ({
127
+ result,
128
+ })),
129
+ )
144
130
  setLoaded(true)
145
131
  }
146
132
  } catch (e) {
@@ -154,125 +140,139 @@ function RefNameAutocomplete({
154
140
  return () => {
155
141
  active = false
156
142
  }
157
- }, [assemblyName, debouncedSearch, session, model])
143
+ }, [assemblyName, fetchResults, debouncedSearch, session, model])
158
144
 
159
145
  const inputBoxVal = coarseVisibleLocStrings || value || ''
160
146
 
161
- // heuristic, text width + icon width, minimum 200
162
- const width = Math.min(Math.max(measureText(inputBoxVal, 16) + 25, 200), 550)
147
+ // heuristic, text width + icon width
148
+ // + 45 accomodates help icon and search icon
149
+ const width = Math.min(
150
+ Math.max(measureText(inputBoxVal, 16) + 45, minWidth),
151
+ 550,
152
+ )
163
153
 
164
154
  // notes on implementation:
165
155
  // The selectOnFocus setting helps highlight the field when clicked
166
156
  return (
167
- <Autocomplete
168
- id={`refNameAutocomplete-${model.id}`}
169
- data-testid="autocomplete"
170
- disableListWrap
171
- disableClearable
172
- PopperComponent={MyPopper}
173
- disabled={!assemblyName}
174
- freeSolo
175
- includeInputInList
176
- selectOnFocus
177
- style={{ ...style, width }}
178
- value={inputBoxVal}
179
- loading={!loaded}
180
- inputValue={inputValue}
181
- onInputChange={(event, newInputValue) => setInputValue(newInputValue)}
182
- loadingText="loading results"
183
- open={open}
184
- onOpen={() => setOpen(true)}
185
- onClose={() => {
186
- setOpen(false)
187
- setLoaded(true)
188
- if (hasDisplayedRegions) {
189
- setCurrentSearch('')
190
- setSearchOptions([])
191
- }
192
- }}
193
- onChange={(_event, selectedOption) => {
194
- if (!selectedOption || !assemblyName) {
195
- return
196
- }
157
+ <>
158
+ <Autocomplete
159
+ id={`refNameAutocomplete-${model.id}`}
160
+ data-testid="autocomplete"
161
+ disableListWrap
162
+ disableClearable
163
+ PopperComponent={MyPopper}
164
+ disabled={!assemblyName}
165
+ freeSolo
166
+ includeInputInList
167
+ selectOnFocus
168
+ style={{ ...style, width }}
169
+ value={inputBoxVal}
170
+ loading={!loaded}
171
+ inputValue={inputValue}
172
+ onInputChange={(event, newInputValue) => setInputValue(newInputValue)}
173
+ loadingText="loading results"
174
+ open={open}
175
+ onOpen={() => setOpen(true)}
176
+ onClose={() => {
177
+ setOpen(false)
178
+ setLoaded(true)
179
+ if (hasDisplayedRegions) {
180
+ setCurrentSearch('')
181
+ setSearchOptions(undefined)
182
+ }
183
+ }}
184
+ onChange={(_event, selectedOption) => {
185
+ if (!selectedOption || !assemblyName) {
186
+ return
187
+ }
197
188
 
198
- if (typeof selectedOption === 'string') {
199
- // handles string inputs on keyPress enter
200
- onSelect(new BaseResult({ label: selectedOption }))
201
- } else {
202
- onSelect(selectedOption.result)
203
- }
204
- setInputValue(inputBoxVal)
205
- }}
206
- options={searchOptions.length === 0 ? options : searchOptions}
207
- getOptionDisabled={option => option?.group === 'limitOption'}
208
- filterOptions={(options, params) => {
209
- const filtered = filterOptions(
210
- options,
211
- params.inputValue.toLocaleLowerCase(),
212
- )
213
- return [
214
- ...filtered.slice(0, 100),
215
- ...(filtered.length > 100
216
- ? [
217
- {
218
- group: 'limitOption',
219
- result: new BaseResult({
220
- label: 'keep typing for more results',
221
- }),
222
- },
223
- ]
224
- : []),
225
- ]
226
- }}
227
- renderInput={params => {
228
- const { helperText, InputProps = {} } = TextFieldProps
229
- return (
230
- <TextField
231
- onBlur={() => {
232
- // this is used to restore a refName or the non-user-typed input
233
- // to the box on blurring
234
- setInputValue(inputBoxVal)
235
- }}
236
- {...params}
237
- {...TextFieldProps}
238
- helperText={helperText}
239
- InputProps={{
240
- ...params.InputProps,
241
- ...InputProps,
189
+ if (typeof selectedOption === 'string') {
190
+ // handles string inputs on keyPress enter
191
+ onSelect(new BaseResult({ label: selectedOption }))
192
+ } else {
193
+ onSelect(selectedOption.result)
194
+ }
195
+ setInputValue(inputBoxVal)
196
+ }}
197
+ options={!searchOptions?.length ? options : searchOptions}
198
+ getOptionDisabled={option => option?.group === 'limitOption'}
199
+ filterOptions={(options, params) => {
200
+ const filtered = filterOptions(
201
+ options,
202
+ params.inputValue.toLocaleLowerCase(),
203
+ )
204
+ return [
205
+ ...filtered.slice(0, 100),
206
+ ...(filtered.length > 100
207
+ ? [
208
+ {
209
+ group: 'limitOption',
210
+ result: new BaseResult({
211
+ label: 'keep typing for more results',
212
+ }),
213
+ },
214
+ ]
215
+ : []),
216
+ ]
217
+ }}
218
+ renderInput={params => {
219
+ const { helperText, InputProps = {} } = TextFieldProps
220
+ return (
221
+ <TextField
222
+ onBlur={() => {
223
+ // this is used to restore a refName or the non-user-typed input
224
+ // to the box on blurring
225
+ setInputValue(inputBoxVal)
226
+ }}
227
+ {...params}
228
+ {...TextFieldProps}
229
+ helperText={helperText}
230
+ InputProps={{
231
+ ...params.InputProps,
232
+ ...InputProps,
242
233
 
243
- endAdornment: (
244
- <>
245
- {regions.length === 0 ? (
246
- <CircularProgress color="inherit" size={20} />
247
- ) : (
248
- <InputAdornment position="end" style={{ marginRight: 7 }}>
249
- <SearchIcon />
250
- </InputAdornment>
251
- )}
252
- {params.InputProps.endAdornment}
253
- </>
254
- ),
255
- }}
256
- placeholder="Search for location"
257
- onChange={e => {
258
- setCurrentSearch(e.target.value)
259
- }}
260
- />
261
- )
262
- }}
263
- renderOption={option => {
264
- const { result } = option
265
- const component = result.getRenderingComponent()
266
- if (component && React.isValidElement(component)) {
267
- return component
268
- }
234
+ endAdornment: (
235
+ <>
236
+ {regions.length === 0 ? (
237
+ <CircularProgress color="inherit" size={20} />
238
+ ) : (
239
+ <InputAdornment position="end" style={{ marginRight: 7 }}>
240
+ <SearchIcon />
241
+ <IconButton
242
+ onClick={() => setHelpDialogDisplayed(true)}
243
+ >
244
+ <HelpIcon />
245
+ </IconButton>
246
+ </InputAdornment>
247
+ )}
248
+ {params.InputProps.endAdornment}
249
+ </>
250
+ ),
251
+ }}
252
+ placeholder="Search for location"
253
+ onChange={e => {
254
+ setCurrentSearch(e.target.value)
255
+ }}
256
+ />
257
+ )
258
+ }}
259
+ renderOption={option => {
260
+ const { result } = option
261
+ const component = result.getRenderingComponent()
262
+ if (component && React.isValidElement(component)) {
263
+ return component
264
+ }
269
265
 
270
- return <Typography noWrap>{result.getDisplayString()}</Typography>
271
- }}
272
- getOptionLabel={option =>
273
- (typeof option === 'string' ? option : option.result.getLabel()) || ''
274
- }
275
- />
266
+ return <Typography noWrap>{result.getDisplayString()}</Typography>
267
+ }}
268
+ getOptionLabel={option =>
269
+ (typeof option === 'string' ? option : option.result.getLabel()) || ''
270
+ }
271
+ />
272
+ {isHelpDialogDisplayed ? (
273
+ <HelpDialog handleClose={() => setHelpDialogDisplayed(false)} />
274
+ ) : null}
275
+ </>
276
276
  )
277
277
  }
278
278
 
@@ -6,7 +6,6 @@ import {
6
6
  Dialog,
7
7
  DialogActions,
8
8
  DialogContent,
9
- DialogContentText,
10
9
  DialogTitle,
11
10
  Divider,
12
11
  IconButton,
@@ -18,10 +17,9 @@ import {
18
17
  TableRow,
19
18
  Typography,
20
19
  Paper,
20
+ makeStyles,
21
21
  } from '@material-ui/core'
22
22
  import CloseIcon from '@material-ui/icons/Close'
23
- import { makeStyles } from '@material-ui/core/styles'
24
- import BaseResult from '@jbrowse/core/TextSearch/BaseResults'
25
23
  import { LinearGenomeViewModel } from '../..'
26
24
 
27
25
  export const useStyles = makeStyles(theme => ({
@@ -101,8 +99,8 @@ export default function SearchResultsDialog({
101
99
 
102
100
  return (
103
101
  <Dialog open maxWidth="xl" onClose={handleClose}>
104
- <DialogTitle id="search-results-dialog">
105
- Search Results
102
+ <DialogTitle>
103
+ Search results
106
104
  {handleClose ? (
107
105
  <IconButton
108
106
  data-testid="close-resultsDialog"
@@ -117,18 +115,15 @@ export default function SearchResultsDialog({
117
115
  </DialogTitle>
118
116
  <Divider />
119
117
  <DialogContent>
120
- {model.searchResults?.length === 0 ||
121
- model.searchResults === undefined ? (
118
+ {!model.searchResults?.length ? (
122
119
  <Typography>
123
- {`No results found for `}
124
- <b>{model.searchQuery}</b>
120
+ No results found for <b>{model.searchQuery}</b>
125
121
  </Typography>
126
122
  ) : (
127
123
  <>
128
- <DialogContentText id="alert-dialog-slide-description">
129
- {`Showing results for `}
130
- <b>{model.searchQuery}</b>
131
- </DialogContentText>
124
+ <Typography>
125
+ Showing results for <b>{model.searchQuery}</b>
126
+ </Typography>
132
127
  <TableContainer component={Paper}>
133
128
  <Table>
134
129
  <TableHead>
@@ -140,8 +135,8 @@ export default function SearchResultsDialog({
140
135
  </TableRow>
141
136
  </TableHead>
142
137
  <TableBody>
143
- {model.searchResults.map((result: BaseResult, index) => (
144
- <TableRow key={`${result.getLabel()}-${index}`}>
138
+ {model.searchResults.map(result => (
139
+ <TableRow key={`${result.getId()}`}>
145
140
  <TableCell component="th" scope="row">
146
141
  {result.getLabel()}
147
142
  </TableCell>
@@ -151,18 +146,6 @@ export default function SearchResultsDialog({
151
146
  <TableCell align="right">
152
147
  {getTrackName(result.getTrackId()) || 'N/A'}
153
148
  </TableCell>
154
- <TableCell align="right">
155
- <Button
156
- onClick={() => {
157
- handleClick(result.getLocation())
158
- handleClose()
159
- }}
160
- color="primary"
161
- variant="contained"
162
- >
163
- Go to location
164
- </Button>
165
- </TableCell>
166
149
  <TableCell align="right">
167
150
  <Button
168
151
  onClick={() => {
@@ -177,7 +160,7 @@ export default function SearchResultsDialog({
177
160
  color="primary"
178
161
  variant="contained"
179
162
  >
180
- Show Track
163
+ Go
181
164
  </Button>
182
165
  </TableCell>
183
166
  </TableRow>
@@ -190,12 +173,7 @@ export default function SearchResultsDialog({
190
173
  </DialogContent>
191
174
  <Divider />
192
175
  <DialogActions>
193
- <Button
194
- onClick={() => {
195
- handleClose()
196
- }}
197
- color="primary"
198
- >
176
+ <Button onClick={() => handleClose()} color="primary">
199
177
  Cancel
200
178
  </Button>
201
179
  </DialogActions>
@@ -17,14 +17,18 @@ import {
17
17
  import { observer } from 'mobx-react'
18
18
  import { saveAs } from 'file-saver'
19
19
  import { Region } from '@jbrowse/core/util/types'
20
- import { readConfObject } from '@jbrowse/core/configuration'
20
+ import { getConf } from '@jbrowse/core/configuration'
21
21
  import copy from 'copy-to-clipboard'
22
- import { ContentCopy as ContentCopyIcon } from '@jbrowse/core/ui/Icons'
23
- import CloseIcon from '@material-ui/icons/Close'
24
- import GetAppIcon from '@material-ui/icons/GetApp'
25
22
  import { getSession } from '@jbrowse/core/util'
26
23
  import { Feature } from '@jbrowse/core/util/simpleFeature'
27
24
  import { formatSeqFasta } from '@jbrowse/core/util/formatFastaStrings'
25
+
26
+ // icons
27
+ import { ContentCopy as ContentCopyIcon } from '@jbrowse/core/ui/Icons'
28
+ import CloseIcon from '@material-ui/icons/Close'
29
+ import GetAppIcon from '@material-ui/icons/GetApp'
30
+
31
+ // locals
28
32
  import { LinearGenomeViewModel } from '..'
29
33
 
30
34
  const useStyles = makeStyles(theme => ({
@@ -71,10 +75,7 @@ async function fetchSequence(
71
75
  if (!assembly) {
72
76
  throw new Error(`assembly ${assemblyName} not found`)
73
77
  }
74
- const adapterConfig = readConfObject(assembly.configuration, [
75
- 'sequence',
76
- 'adapter',
77
- ])
78
+ const adapterConfig = getConf(assembly, ['sequence', 'adapter'])
78
79
 
79
80
  const sessionId = 'getSequence'
80
81
  const chunks = (await Promise.all(