@jbrowse/plugin-linear-genome-view 1.5.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jbrowse/plugin-linear-genome-view",
3
- "version": "1.5.1",
3
+ "version": "1.5.2",
4
4
  "description": "JBrowse 2 linear genome view",
5
5
  "keywords": [
6
6
  "jbrowse",
@@ -61,5 +61,5 @@
61
61
  "publishConfig": {
62
62
  "access": "public"
63
63
  },
64
- "gitHead": "284e408c72a3d4d7a0d603197501a8fc8f68c4bc"
64
+ "gitHead": "94fdfbc34787ab8f12a87e00038da74b247b42fa"
65
65
  }
@@ -0,0 +1,81 @@
1
+ import React from 'react'
2
+ import {
3
+ Button,
4
+ Dialog,
5
+ DialogActions,
6
+ DialogContent,
7
+ DialogTitle,
8
+ Divider,
9
+ IconButton,
10
+ makeStyles,
11
+ } from '@material-ui/core'
12
+ import CloseIcon from '@material-ui/icons/Close'
13
+
14
+ export const useStyles = makeStyles(theme => ({
15
+ closeButton: {
16
+ position: 'absolute',
17
+ right: theme.spacing(1),
18
+ top: theme.spacing(1),
19
+ color: theme.palette.grey[500],
20
+ },
21
+ }))
22
+
23
+ export default function HelpDialog({
24
+ handleClose,
25
+ }: {
26
+ handleClose: () => void
27
+ }) {
28
+ const classes = useStyles()
29
+ return (
30
+ <Dialog open maxWidth="xl" onClose={handleClose}>
31
+ <DialogTitle>
32
+ Using the search box
33
+ {handleClose ? (
34
+ <IconButton
35
+ data-testid="close-resultsDialog"
36
+ className={classes.closeButton}
37
+ onClick={() => {
38
+ handleClose()
39
+ }}
40
+ >
41
+ <CloseIcon />
42
+ </IconButton>
43
+ ) : null}
44
+ </DialogTitle>
45
+ <Divider />
46
+ <DialogContent>
47
+ <h3>Searching</h3>
48
+ <ul>
49
+ <li>
50
+ Jump to a feature or reference sequence by typing its name in the
51
+ location box and pressing Enter.
52
+ </li>
53
+ <li>
54
+ Jump to a specific region by typing the region into the location box
55
+ as: <code>ref:start..end</code> or <code>ref:start-end</code>.
56
+ Commas are allowed in the start and end coordinates.
57
+ </li>
58
+ </ul>
59
+ <h3>Example Searches</h3>
60
+ <ul>
61
+ <li>
62
+ <code>BRCA</code> - searches for the feature named BRCA
63
+ </li>
64
+ <li>
65
+ <code>chr4</code> - jumps to chromosome 4
66
+ </li>
67
+ <li>
68
+ <code>chr4:79,500,000..80,000,000</code> - jumps the region on
69
+ chromosome 4 between 79.5Mb and 80Mb.
70
+ </li>
71
+ </ul>
72
+ </DialogContent>
73
+ <Divider />
74
+ <DialogActions>
75
+ <Button onClick={() => handleClose()} color="primary">
76
+ Close
77
+ </Button>
78
+ </DialogActions>
79
+ </Dialog>
80
+ )
81
+ }
@@ -1,4 +1,4 @@
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
4
  import {
@@ -6,17 +6,18 @@ import {
6
6
  CircularProgress,
7
7
  Container,
8
8
  Grid,
9
- Typography,
10
9
  makeStyles,
11
10
  } from '@material-ui/core'
12
11
  import { SearchType } from '@jbrowse/core/data_adapters/BaseAdapter'
13
12
  import BaseResult from '@jbrowse/core/TextSearch/BaseResults'
14
13
  import AssemblySelector from '@jbrowse/core/ui/AssemblySelector'
14
+ import ErrorMessage from '@jbrowse/core/ui/ErrorMessage'
15
+ import CloseIcon from '@material-ui/icons/Close'
15
16
 
16
17
  // locals
17
18
  import RefNameAutocomplete from './RefNameAutocomplete'
18
- import SearchResultsDialog from './SearchResultsDialog'
19
19
  import { LinearGenomeViewModel } from '..'
20
+ const SearchResultsDialog = lazy(() => import('./SearchResultsDialog'))
20
21
 
21
22
  const useStyles = makeStyles(theme => ({
22
23
  importFormContainer: {
@@ -29,14 +30,6 @@ const useStyles = makeStyles(theme => ({
29
30
 
30
31
  type LGV = LinearGenomeViewModel
31
32
 
32
- const ErrorDisplay = observer(({ error }: { error?: Error | string }) => {
33
- return (
34
- <Typography variant="h6" color="error">
35
- {`${error}`}
36
- </Typography>
37
- )
38
- })
39
-
40
33
  const ImportForm = observer(({ model }: { model: LGV }) => {
41
34
  const classes = useStyles()
42
35
  const session = getSession(model)
@@ -91,13 +84,11 @@ const ImportForm = observer(({ model }: { model: LGV }) => {
91
84
  return [...(refNameResults || []), ...(textSearchResults || [])]
92
85
  }
93
86
 
94
- /**
95
- * gets a string as input, or use stored option results from previous query,
96
- * then re-query and
97
- * 1) if it has multiple results: pop a dialog
98
- * 2) if it's a single result navigate to it
99
- * 3) else assume it's a locstring and navigate to it
100
- */
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
101
92
  async function handleSelectedRegion(input: string) {
102
93
  if (!option) {
103
94
  return
@@ -132,13 +123,9 @@ const ImportForm = observer(({ model }: { model: LGV }) => {
132
123
  // having this wrapped in a form allows intuitive use of enter key to submit
133
124
  return (
134
125
  <div>
135
- {err ? <ErrorDisplay error={err} /> : null}
126
+ {err ? <ErrorMessage error={err} /> : null}
136
127
  <Container className={classes.importFormContainer}>
137
- <form
138
- onSubmit={event => {
139
- event.preventDefault()
140
- }}
141
- >
128
+ <form onSubmit={event => event.preventDefault()}>
142
129
  <Grid
143
130
  container
144
131
  spacing={1}
@@ -158,18 +145,21 @@ const ImportForm = observer(({ model }: { model: LGV }) => {
158
145
  <Grid item>
159
146
  {selectedAsm ? (
160
147
  err ? (
161
- <Typography color="error">X</Typography>
162
- ) : selectedRegion && model.volatileWidth ? (
148
+ <CloseIcon style={{ color: 'red' }} />
149
+ ) : selectedRegion ? (
163
150
  <RefNameAutocomplete
164
151
  fetchResults={fetchResults}
165
152
  model={model}
166
153
  assemblyName={assemblyError ? undefined : selectedAsm}
167
154
  value={selectedRegion}
155
+ // note: minWidth 270 accomodates full width of helperText
156
+ minWidth={270}
168
157
  onSelect={option => setOption(option)}
169
158
  TextFieldProps={{
170
159
  margin: 'normal',
171
160
  variant: 'outlined',
172
- helperText: 'Enter a sequence or location',
161
+ helperText:
162
+ 'Enter sequence name, feature name, or location',
173
163
  }}
174
164
  />
175
165
  ) : (
@@ -181,6 +171,7 @@ const ImportForm = observer(({ model }: { model: LGV }) => {
181
171
  )
182
172
  ) : null}
183
173
  </Grid>
174
+ <Grid item></Grid>
184
175
  <Grid item>
185
176
  <Button
186
177
  type="submit"
@@ -177,7 +177,7 @@ const SVGHeader = ({ model }: { model: LGV }) => {
177
177
  <Cytobands overview={overview} assembly={assembly} block={block} />
178
178
  <rect
179
179
  stroke="red"
180
- fill="none"
180
+ fill="rgb(255,0,0,0.1)"
181
181
  width={Math.max(lastOverviewPx - firstOverviewPx, 0.5)}
182
182
  height={HEADER_OVERVIEW_HEIGHT - 1}
183
183
  x={firstOverviewPx}
@@ -143,7 +143,7 @@ function OverviewRubberBand({
143
143
  startX !== undefined &&
144
144
  currentX === undefined
145
145
  ) {
146
- const clickedAt = overview.pxToBp(startX)
146
+ const clickedAt = overview.pxToBp(startX - cytobandOffset)
147
147
  model.centerAt(
148
148
  Math.round(clickedAt.coord),
149
149
  clickedAt.refName,
@@ -62,7 +62,7 @@ const useStyles = makeStyles(theme => {
62
62
  height: HEADER_OVERVIEW_HEIGHT,
63
63
  pointerEvents: 'none',
64
64
  zIndex: 100,
65
- border: '1px solid red',
65
+ border: '1px solid',
66
66
  },
67
67
  overview: {
68
68
  height: HEADER_BAR_HEIGHT,
@@ -405,6 +405,9 @@ const ScaleBar = observer(
405
405
  coord: last.reversed ? last.start : last.end,
406
406
  }) || 0
407
407
 
408
+ const color = showCytobands ? '#f00' : scaleBarColor
409
+ const transparency = showCytobands ? 0.1 : 0.3
410
+
408
411
  return (
409
412
  <div className={classes.scaleBar}>
410
413
  <div
@@ -412,7 +415,8 @@ const ScaleBar = observer(
412
415
  style={{
413
416
  width: lastOverviewPx - firstOverviewPx,
414
417
  left: firstOverviewPx + cytobandOffset,
415
- background: showCytobands ? undefined : alpha(scaleBarColor, 0.3),
418
+ background: alpha(color, transparency),
419
+ borderColor: color,
416
420
  }}
417
421
  />
418
422
  {/* this is the entire scale bar */}
@@ -1,31 +1,32 @@
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'
20
17
 
21
18
  // icons
22
19
  import SearchIcon from '@material-ui/icons/Search'
23
20
  import Autocomplete from '@material-ui/lab/Autocomplete'
21
+ import HelpIcon from '@material-ui/icons/Help'
24
22
 
25
23
  // locals
26
24
  import { LinearGenomeViewModel } from '..'
27
25
  import { dedupe } from './util'
28
26
 
27
+ // lazy
28
+ const HelpDialog = lazy(() => import('./HelpDialog'))
29
+
29
30
  export interface Option {
30
31
  group?: string
31
32
  result: BaseResult
@@ -70,6 +71,7 @@ function RefNameAutocomplete({
70
71
  style,
71
72
  fetchResults,
72
73
  value,
74
+ minWidth = 200,
73
75
  TextFieldProps = {},
74
76
  }: {
75
77
  model: LinearGenomeViewModel
@@ -78,12 +80,14 @@ function RefNameAutocomplete({
78
80
  value?: string
79
81
  fetchResults: (query: string) => Promise<BaseResult[]>
80
82
  style?: React.CSSProperties
83
+ minWidth?: number
81
84
  TextFieldProps?: TFP
82
85
  }) {
83
86
  const session = getSession(model)
84
87
  const { assemblyManager } = session
85
88
  const [open, setOpen] = useState(false)
86
89
  const [loaded, setLoaded] = useState(true)
90
+ const [isHelpDialogDisplayed, setHelpDialogDisplayed] = useState(false)
87
91
  const [currentSearch, setCurrentSearch] = useState('')
88
92
  const [inputValue, setInputValue] = useState('')
89
93
  const [searchOptions, setSearchOptions] = useState<Option[]>()
@@ -140,121 +144,135 @@ function RefNameAutocomplete({
140
144
 
141
145
  const inputBoxVal = coarseVisibleLocStrings || value || ''
142
146
 
143
- // heuristic, text width + icon width, minimum 200
144
- 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
+ )
145
153
 
146
154
  // notes on implementation:
147
155
  // The selectOnFocus setting helps highlight the field when clicked
148
156
  return (
149
- <Autocomplete
150
- id={`refNameAutocomplete-${model.id}`}
151
- data-testid="autocomplete"
152
- disableListWrap
153
- disableClearable
154
- PopperComponent={MyPopper}
155
- disabled={!assemblyName}
156
- freeSolo
157
- includeInputInList
158
- selectOnFocus
159
- style={{ ...style, width }}
160
- value={inputBoxVal}
161
- loading={!loaded}
162
- inputValue={inputValue}
163
- onInputChange={(event, newInputValue) => setInputValue(newInputValue)}
164
- loadingText="loading results"
165
- open={open}
166
- onOpen={() => setOpen(true)}
167
- onClose={() => {
168
- setOpen(false)
169
- setLoaded(true)
170
- if (hasDisplayedRegions) {
171
- setCurrentSearch('')
172
- setSearchOptions(undefined)
173
- }
174
- }}
175
- onChange={(_event, selectedOption) => {
176
- if (!selectedOption || !assemblyName) {
177
- return
178
- }
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
+ }
179
188
 
180
- if (typeof selectedOption === 'string') {
181
- // handles string inputs on keyPress enter
182
- onSelect(new BaseResult({ label: selectedOption }))
183
- } else {
184
- onSelect(selectedOption.result)
185
- }
186
- setInputValue(inputBoxVal)
187
- }}
188
- options={!searchOptions?.length ? options : searchOptions}
189
- getOptionDisabled={option => option?.group === 'limitOption'}
190
- filterOptions={(options, params) => {
191
- const filtered = filterOptions(
192
- options,
193
- params.inputValue.toLocaleLowerCase(),
194
- )
195
- return [
196
- ...filtered.slice(0, 100),
197
- ...(filtered.length > 100
198
- ? [
199
- {
200
- group: 'limitOption',
201
- result: new BaseResult({
202
- label: 'keep typing for more results',
203
- }),
204
- },
205
- ]
206
- : []),
207
- ]
208
- }}
209
- renderInput={params => {
210
- const { helperText, InputProps = {} } = TextFieldProps
211
- return (
212
- <TextField
213
- onBlur={() => {
214
- // this is used to restore a refName or the non-user-typed input
215
- // to the box on blurring
216
- setInputValue(inputBoxVal)
217
- }}
218
- {...params}
219
- {...TextFieldProps}
220
- helperText={helperText}
221
- InputProps={{
222
- ...params.InputProps,
223
- ...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,
224
233
 
225
- endAdornment: (
226
- <>
227
- {regions.length === 0 ? (
228
- <CircularProgress color="inherit" size={20} />
229
- ) : (
230
- <InputAdornment position="end" style={{ marginRight: 7 }}>
231
- <SearchIcon />
232
- </InputAdornment>
233
- )}
234
- {params.InputProps.endAdornment}
235
- </>
236
- ),
237
- }}
238
- placeholder="Search for location"
239
- onChange={e => {
240
- setCurrentSearch(e.target.value)
241
- }}
242
- />
243
- )
244
- }}
245
- renderOption={option => {
246
- const { result } = option
247
- const component = result.getRenderingComponent()
248
- if (component && React.isValidElement(component)) {
249
- return component
250
- }
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
+ }
251
265
 
252
- return <Typography noWrap>{result.getDisplayString()}</Typography>
253
- }}
254
- getOptionLabel={option =>
255
- (typeof option === 'string' ? option : option.result.getLabel()) || ''
256
- }
257
- />
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
+ </>
258
276
  )
259
277
  }
260
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>