@jbrowse/plugin-linear-genome-view 1.7.7 → 1.7.10
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/BaseLinearDisplay.d.ts +1 -5
- package/dist/BaseLinearDisplay/components/BaseLinearDisplay.js +32 -120
- package/dist/BaseLinearDisplay/components/Tooltip.d.ts +8 -0
- package/dist/BaseLinearDisplay/components/Tooltip.js +125 -0
- package/dist/BaseLinearDisplay/models/BaseLinearDisplayModel.d.ts +3 -3
- package/dist/BaseLinearDisplay/models/BaseLinearDisplayModel.js +3 -4
- package/dist/LinearGenomeView/components/ExportSvgDialog.js +35 -25
- package/dist/LinearGenomeView/components/Header.js +5 -2
- package/dist/LinearGenomeView/components/HelpDialog.js +2 -3
- package/dist/LinearGenomeView/components/ImportForm.js +47 -47
- package/dist/LinearGenomeView/components/LinearGenomeView.js +6 -2
- package/dist/LinearGenomeView/components/LinearGenomeView.test.js +2 -2
- package/dist/LinearGenomeView/components/OverviewScaleBar.js +2 -2
- package/dist/LinearGenomeView/components/RefNameAutocomplete.d.ts +3 -2
- package/dist/LinearGenomeView/components/RefNameAutocomplete.js +7 -5
- package/dist/LinearGenomeView/components/ScaleBar.d.ts +8 -4
- package/dist/LinearGenomeView/components/ScaleBar.js +8 -3
- package/dist/LinearGenomeView/components/SearchBox.js +31 -22
- package/dist/LinearGenomeView/components/TrackLabel.js +25 -41
- package/dist/LinearGenomeView/index.d.ts +7 -11
- package/dist/LinearGenomeView/index.js +60 -33
- package/dist/LinearGenomeView/index.test.js +22 -5
- package/dist/index.js +22 -11
- package/package.json +3 -2
- package/src/BaseLinearDisplay/components/BaseLinearDisplay.tsx +4 -89
- package/src/BaseLinearDisplay/components/Tooltip.tsx +97 -0
- package/src/BaseLinearDisplay/models/BaseLinearDisplayModel.tsx +11 -4
- package/src/LinearGenomeView/components/ExportSvgDialog.tsx +24 -11
- package/src/LinearGenomeView/components/Header.tsx +3 -2
- package/src/LinearGenomeView/components/HelpDialog.tsx +5 -4
- package/src/LinearGenomeView/components/ImportForm.tsx +37 -32
- package/src/LinearGenomeView/components/LinearGenomeView.test.js +2 -2
- package/src/LinearGenomeView/components/LinearGenomeView.tsx +16 -10
- package/src/LinearGenomeView/components/OverviewScaleBar.tsx +3 -4
- package/src/LinearGenomeView/components/RefNameAutocomplete.tsx +10 -5
- package/src/LinearGenomeView/components/ScaleBar.tsx +6 -9
- package/src/LinearGenomeView/components/SearchBox.tsx +20 -4
- package/src/LinearGenomeView/components/TrackLabel.tsx +25 -28
- package/src/LinearGenomeView/components/__snapshots__/LinearGenomeView.test.js.snap +4 -21
- package/src/LinearGenomeView/index.test.ts +20 -5
- package/src/LinearGenomeView/index.tsx +56 -27
- package/src/index.ts +35 -30
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import React, { useState, useMemo } from 'react'
|
|
2
|
+
import { getConf } from '@jbrowse/core/configuration'
|
|
3
|
+
import { observer } from 'mobx-react'
|
|
4
|
+
import { Portal, alpha, makeStyles } from '@material-ui/core'
|
|
5
|
+
import { usePopper } from 'react-popper'
|
|
6
|
+
|
|
7
|
+
// locals
|
|
8
|
+
import { BaseLinearDisplayModel } from '../models/BaseLinearDisplayModel'
|
|
9
|
+
|
|
10
|
+
function round(value: number) {
|
|
11
|
+
return Math.round(value * 1e5) / 1e5
|
|
12
|
+
}
|
|
13
|
+
const useStyles = makeStyles(theme => ({
|
|
14
|
+
// these styles come from
|
|
15
|
+
// https://github.com/mui-org/material-ui/blob/master/packages/material-ui/src/Tooltip/Tooltip.js
|
|
16
|
+
tooltip: {
|
|
17
|
+
pointerEvents: 'none',
|
|
18
|
+
backgroundColor: alpha(theme.palette.grey[700], 0.9),
|
|
19
|
+
borderRadius: theme.shape.borderRadius,
|
|
20
|
+
color: theme.palette.common.white,
|
|
21
|
+
fontFamily: theme.typography.fontFamily,
|
|
22
|
+
padding: '4px 8px',
|
|
23
|
+
fontSize: theme.typography.pxToRem(12),
|
|
24
|
+
lineHeight: `${round(14 / 10)}em`,
|
|
25
|
+
maxWidth: 300,
|
|
26
|
+
wordWrap: 'break-word',
|
|
27
|
+
},
|
|
28
|
+
}))
|
|
29
|
+
|
|
30
|
+
const TooltipContents = React.forwardRef<
|
|
31
|
+
HTMLDivElement,
|
|
32
|
+
{ message: React.ReactNode | string }
|
|
33
|
+
>(({ message }: { message: React.ReactNode | string }, ref) => {
|
|
34
|
+
return <div ref={ref}>{message}</div>
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
type Coord = [number, number]
|
|
38
|
+
const Tooltip = observer(
|
|
39
|
+
({
|
|
40
|
+
model,
|
|
41
|
+
clientMouseCoord,
|
|
42
|
+
}: {
|
|
43
|
+
model: BaseLinearDisplayModel
|
|
44
|
+
clientMouseCoord: Coord
|
|
45
|
+
}) => {
|
|
46
|
+
const classes = useStyles()
|
|
47
|
+
const { featureUnderMouse } = model
|
|
48
|
+
const [width, setWidth] = useState(0)
|
|
49
|
+
const [popperElt, setPopperElt] = useState<HTMLDivElement | null>(null)
|
|
50
|
+
|
|
51
|
+
// must be memoized a la https://github.com/popperjs/react-popper/issues/391
|
|
52
|
+
const virtElement = useMemo(
|
|
53
|
+
() => ({
|
|
54
|
+
getBoundingClientRect: () => {
|
|
55
|
+
const x = clientMouseCoord[0] + width / 2 + 20
|
|
56
|
+
const y = clientMouseCoord[1]
|
|
57
|
+
return {
|
|
58
|
+
top: y,
|
|
59
|
+
left: x,
|
|
60
|
+
bottom: y,
|
|
61
|
+
right: x,
|
|
62
|
+
width: 0,
|
|
63
|
+
height: 0,
|
|
64
|
+
x,
|
|
65
|
+
y,
|
|
66
|
+
toJSON() {},
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
}),
|
|
70
|
+
[clientMouseCoord, width],
|
|
71
|
+
)
|
|
72
|
+
const { styles, attributes } = usePopper(virtElement, popperElt)
|
|
73
|
+
|
|
74
|
+
const contents = featureUnderMouse
|
|
75
|
+
? getConf(model, 'mouseover', { feature: featureUnderMouse })
|
|
76
|
+
: undefined
|
|
77
|
+
|
|
78
|
+
return featureUnderMouse && contents ? (
|
|
79
|
+
<Portal>
|
|
80
|
+
<div
|
|
81
|
+
ref={setPopperElt}
|
|
82
|
+
className={classes.tooltip}
|
|
83
|
+
// zIndex needed to go over widget drawer
|
|
84
|
+
style={{ ...styles.popper, zIndex: 100000 }}
|
|
85
|
+
{...attributes.popper}
|
|
86
|
+
>
|
|
87
|
+
<TooltipContents
|
|
88
|
+
ref={elt => setWidth(elt?.getBoundingClientRect().width || 0)}
|
|
89
|
+
message={contents}
|
|
90
|
+
/>
|
|
91
|
+
</div>
|
|
92
|
+
</Portal>
|
|
93
|
+
) : null
|
|
94
|
+
},
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
export default Tooltip
|
|
@@ -7,6 +7,7 @@ import { MenuItem } from '@jbrowse/core/ui'
|
|
|
7
7
|
import {
|
|
8
8
|
isAbortException,
|
|
9
9
|
getContainingView,
|
|
10
|
+
getContainingTrack,
|
|
10
11
|
getSession,
|
|
11
12
|
getViewParams,
|
|
12
13
|
isSelectionContainer,
|
|
@@ -333,8 +334,13 @@ export const BaseLinearDisplay = types
|
|
|
333
334
|
const featureWidget = session.addWidget(
|
|
334
335
|
'BaseFeatureWidget',
|
|
335
336
|
'baseFeature',
|
|
336
|
-
{
|
|
337
|
+
{
|
|
338
|
+
view: getContainingView(self),
|
|
339
|
+
track: getContainingTrack(self),
|
|
340
|
+
featureData: feature.toJSON(),
|
|
341
|
+
},
|
|
337
342
|
)
|
|
343
|
+
|
|
338
344
|
session.showWidget(featureWidget)
|
|
339
345
|
}
|
|
340
346
|
if (isSelectionContainer(session)) {
|
|
@@ -551,7 +557,7 @@ export const BaseLinearDisplay = types
|
|
|
551
557
|
self.currBpPerPx !== view.bpPerPx || !self.estimatedRegionStats,
|
|
552
558
|
rpcDriverName: self.rpcDriverName,
|
|
553
559
|
displayModel: self,
|
|
554
|
-
onFeatureClick(_: unknown, featureId
|
|
560
|
+
onFeatureClick(_: unknown, featureId?: string) {
|
|
555
561
|
const f = featureId || self.featureIdUnderMouse
|
|
556
562
|
if (!f) {
|
|
557
563
|
self.clearFeatureSelection()
|
|
@@ -566,7 +572,7 @@ export const BaseLinearDisplay = types
|
|
|
566
572
|
self.clearFeatureSelection()
|
|
567
573
|
},
|
|
568
574
|
// similar to click but opens a menu with further options
|
|
569
|
-
onFeatureContextMenu(_: unknown, featureId
|
|
575
|
+
onFeatureContextMenu(_: unknown, featureId?: string) {
|
|
570
576
|
const f = featureId || self.featureIdUnderMouse
|
|
571
577
|
if (!f) {
|
|
572
578
|
self.clearFeatureSelection()
|
|
@@ -576,7 +582,7 @@ export const BaseLinearDisplay = types
|
|
|
576
582
|
}
|
|
577
583
|
},
|
|
578
584
|
|
|
579
|
-
onMouseMove(_: unknown, featureId
|
|
585
|
+
onMouseMove(_: unknown, featureId?: string) {
|
|
580
586
|
self.setFeatureIdUnderMouse(featureId)
|
|
581
587
|
},
|
|
582
588
|
|
|
@@ -642,6 +648,7 @@ export const BaseLinearDisplay = types
|
|
|
642
648
|
const { offsetPx } = roundedDynamicBlocks[index]
|
|
643
649
|
const offset = offsetPx - viewOffsetPx
|
|
644
650
|
const clipid = getId(id, index)
|
|
651
|
+
|
|
645
652
|
return (
|
|
646
653
|
<React.Fragment key={`frag-${index}`}>
|
|
647
654
|
<defs>
|
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import React, { useState } from 'react'
|
|
2
|
-
import { makeStyles } from '@material-ui/core/styles'
|
|
3
2
|
import {
|
|
4
3
|
Button,
|
|
4
|
+
Checkbox,
|
|
5
|
+
CircularProgress,
|
|
5
6
|
Dialog,
|
|
6
7
|
DialogActions,
|
|
7
8
|
DialogContent,
|
|
8
9
|
DialogTitle,
|
|
9
|
-
IconButton,
|
|
10
|
-
Checkbox,
|
|
11
10
|
FormControlLabel,
|
|
12
|
-
|
|
11
|
+
IconButton,
|
|
12
|
+
TextField,
|
|
13
13
|
Typography,
|
|
14
|
+
makeStyles,
|
|
14
15
|
} from '@material-ui/core'
|
|
16
|
+
import { ErrorMessage } from '@jbrowse/core/ui'
|
|
15
17
|
import CloseIcon from '@material-ui/icons/Close'
|
|
16
18
|
import { LinearGenomeViewModel as LGV } from '..'
|
|
17
19
|
|
|
@@ -24,6 +26,15 @@ const useStyles = makeStyles(theme => ({
|
|
|
24
26
|
},
|
|
25
27
|
}))
|
|
26
28
|
|
|
29
|
+
function LoadingMessage() {
|
|
30
|
+
return (
|
|
31
|
+
<div>
|
|
32
|
+
<CircularProgress size={20} style={{ marginRight: 20 }} />
|
|
33
|
+
<Typography display="inline">Creating SVG</Typography>
|
|
34
|
+
</div>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
27
38
|
export default function ExportSvgDlg({
|
|
28
39
|
model,
|
|
29
40
|
handleClose,
|
|
@@ -35,6 +46,7 @@ export default function ExportSvgDlg({
|
|
|
35
46
|
const offscreenCanvas = typeof OffscreenCanvas !== 'undefined'
|
|
36
47
|
const [rasterizeLayers, setRasterizeLayers] = useState(offscreenCanvas)
|
|
37
48
|
const [loading, setLoading] = useState(false)
|
|
49
|
+
const [filename, setFilename] = useState('jbrowse.svg')
|
|
38
50
|
const [error, setError] = useState<unknown>()
|
|
39
51
|
const classes = useStyles()
|
|
40
52
|
return (
|
|
@@ -47,13 +59,15 @@ export default function ExportSvgDlg({
|
|
|
47
59
|
</DialogTitle>
|
|
48
60
|
<DialogContent>
|
|
49
61
|
{error ? (
|
|
50
|
-
<
|
|
62
|
+
<ErrorMessage error={error} />
|
|
51
63
|
) : loading ? (
|
|
52
|
-
<
|
|
53
|
-
<CircularProgress size={20} style={{ marginRight: 20 }} />
|
|
54
|
-
<Typography display="inline">Creating SVG</Typography>
|
|
55
|
-
</div>
|
|
64
|
+
<LoadingMessage />
|
|
56
65
|
) : null}
|
|
66
|
+
<TextField
|
|
67
|
+
helperText="filename"
|
|
68
|
+
value={filename}
|
|
69
|
+
onChange={event => setFilename(event.target.value)}
|
|
70
|
+
/>
|
|
57
71
|
{offscreenCanvas ? (
|
|
58
72
|
<FormControlLabel
|
|
59
73
|
control={
|
|
@@ -87,12 +101,11 @@ export default function ExportSvgDlg({
|
|
|
87
101
|
setLoading(true)
|
|
88
102
|
setError(undefined)
|
|
89
103
|
try {
|
|
90
|
-
await model.exportSvg({ rasterizeLayers })
|
|
104
|
+
await model.exportSvg({ rasterizeLayers, filename })
|
|
91
105
|
handleClose()
|
|
92
106
|
} catch (e) {
|
|
93
107
|
console.error(e)
|
|
94
108
|
setError(e)
|
|
95
|
-
} finally {
|
|
96
109
|
setLoading(false)
|
|
97
110
|
}
|
|
98
111
|
}}
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
makeStyles,
|
|
8
8
|
alpha,
|
|
9
9
|
} from '@material-ui/core'
|
|
10
|
+
import { getTickDisplayStr2 } from '@jbrowse/core/util'
|
|
10
11
|
import SearchBox from './SearchBox'
|
|
11
12
|
|
|
12
13
|
// icons
|
|
@@ -97,10 +98,10 @@ function PanControls({ model }: { model: LGV }) {
|
|
|
97
98
|
|
|
98
99
|
const RegionWidth = observer(({ model }: { model: LGV }) => {
|
|
99
100
|
const classes = useStyles()
|
|
100
|
-
const { coarseTotalBp } = model
|
|
101
|
+
const { coarseTotalBp, bpPerPx } = model
|
|
101
102
|
return (
|
|
102
103
|
<Typography variant="body2" color="textSecondary" className={classes.bp}>
|
|
103
|
-
{
|
|
104
|
+
{getTickDisplayStr2(coarseTotalBp, bpPerPx)}
|
|
104
105
|
</Typography>
|
|
105
106
|
)
|
|
106
107
|
})
|
|
@@ -32,11 +32,8 @@ export default function HelpDialog({
|
|
|
32
32
|
Using the search box
|
|
33
33
|
{handleClose ? (
|
|
34
34
|
<IconButton
|
|
35
|
-
data-testid="close-resultsDialog"
|
|
36
35
|
className={classes.closeButton}
|
|
37
|
-
onClick={() =>
|
|
38
|
-
handleClose()
|
|
39
|
-
}}
|
|
36
|
+
onClick={() => handleClose()}
|
|
40
37
|
>
|
|
41
38
|
<CloseIcon />
|
|
42
39
|
</IconButton>
|
|
@@ -81,6 +78,10 @@ export default function HelpDialog({
|
|
|
81
78
|
<code>chr1:1-100[rev] chr2:1-100</code> - open up the first region
|
|
82
79
|
in the horizontally flipped orientation
|
|
83
80
|
</li>
|
|
81
|
+
<li>
|
|
82
|
+
<code>chr1 100 200</code> - use whitespace separated refname, start,
|
|
83
|
+
end
|
|
84
|
+
</li>
|
|
84
85
|
</ul>
|
|
85
86
|
</DialogContent>
|
|
86
87
|
<Divider />
|
|
@@ -37,13 +37,9 @@ const ImportForm = observer(({ model }: { model: LGV }) => {
|
|
|
37
37
|
const classes = useStyles()
|
|
38
38
|
const session = getSession(model)
|
|
39
39
|
const { assemblyNames, assemblyManager, textSearchManager } = session
|
|
40
|
-
const {
|
|
41
|
-
rankSearchResults,
|
|
42
|
-
isSearchDialogDisplayed,
|
|
43
|
-
error: modelError,
|
|
44
|
-
} = model
|
|
40
|
+
const { rankSearchResults, isSearchDialogDisplayed, error } = model
|
|
45
41
|
const [selectedAsm, setSelectedAsm] = useState(assemblyNames[0])
|
|
46
|
-
const [
|
|
42
|
+
const [importError, setImportError] = useState(error)
|
|
47
43
|
const searchScope = model.searchScope(selectedAsm)
|
|
48
44
|
|
|
49
45
|
const assembly = assemblyManager.get(selectedAsm)
|
|
@@ -51,19 +47,15 @@ const ImportForm = observer(({ model }: { model: LGV }) => {
|
|
|
51
47
|
? assembly?.error
|
|
52
48
|
: 'No configured assemblies'
|
|
53
49
|
const regions = assembly?.regions || []
|
|
54
|
-
const err = assemblyError ||
|
|
55
|
-
|
|
56
|
-
const
|
|
50
|
+
const err = assemblyError || importError
|
|
51
|
+
const [myVal, setValue] = useState('')
|
|
52
|
+
const value = myVal || regions[0]?.refName
|
|
57
53
|
|
|
58
54
|
// use this instead of useState initializer because the useState initializer
|
|
59
55
|
// won't update in response to an observable
|
|
60
|
-
const option =
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
label: regions[0]?.refName,
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
const selectedRegion = option?.getLocation()
|
|
56
|
+
const option = new BaseResult({
|
|
57
|
+
label: value,
|
|
58
|
+
})
|
|
67
59
|
|
|
68
60
|
async function fetchResults(query: string, searchType?: SearchType) {
|
|
69
61
|
if (!textSearchManager) {
|
|
@@ -98,15 +90,26 @@ const ImportForm = observer(({ model }: { model: LGV }) => {
|
|
|
98
90
|
}
|
|
99
91
|
let trackId = option.getTrackId()
|
|
100
92
|
let location = input || option.getLocation() || ''
|
|
93
|
+
const [ref, rest] = location.split(':')
|
|
94
|
+
const allRefs = assembly?.allRefNames || []
|
|
101
95
|
try {
|
|
102
|
-
|
|
96
|
+
// instead of querying text-index, first:
|
|
97
|
+
// - check if input matches a refname directly
|
|
98
|
+
// - or looks like locstring
|
|
99
|
+
// then just navigate as if it were a locstring
|
|
100
|
+
if (
|
|
101
|
+
allRefs.includes(location) ||
|
|
102
|
+
(allRefs.includes(ref) &&
|
|
103
|
+
rest !== undefined &&
|
|
104
|
+
!Number.isNaN(parseInt(rest, 10)))
|
|
105
|
+
) {
|
|
103
106
|
model.navToLocString(location, selectedAsm)
|
|
104
107
|
} else {
|
|
105
108
|
const results = await fetchResults(input, 'exact')
|
|
106
|
-
if (results
|
|
109
|
+
if (results.length > 1) {
|
|
107
110
|
model.setSearchResults(results, input.toLowerCase())
|
|
108
111
|
return
|
|
109
|
-
} else if (results
|
|
112
|
+
} else if (results.length === 1) {
|
|
110
113
|
location = results[0].getLocation()
|
|
111
114
|
trackId = results[0].getTrackId()
|
|
112
115
|
}
|
|
@@ -130,7 +133,15 @@ const ImportForm = observer(({ model }: { model: LGV }) => {
|
|
|
130
133
|
<div className={classes.container}>
|
|
131
134
|
{err ? <ErrorMessage error={err} /> : null}
|
|
132
135
|
<Container className={classes.importFormContainer}>
|
|
133
|
-
<form
|
|
136
|
+
<form
|
|
137
|
+
onSubmit={event => {
|
|
138
|
+
event.preventDefault()
|
|
139
|
+
model.setError(undefined)
|
|
140
|
+
if (value) {
|
|
141
|
+
handleSelectedRegion(value)
|
|
142
|
+
}
|
|
143
|
+
}}
|
|
144
|
+
>
|
|
134
145
|
<Grid
|
|
135
146
|
container
|
|
136
147
|
spacing={1}
|
|
@@ -140,7 +151,7 @@ const ImportForm = observer(({ model }: { model: LGV }) => {
|
|
|
140
151
|
<Grid item>
|
|
141
152
|
<AssemblySelector
|
|
142
153
|
onChange={val => {
|
|
143
|
-
|
|
154
|
+
setImportError('')
|
|
144
155
|
setSelectedAsm(val)
|
|
145
156
|
}}
|
|
146
157
|
session={session}
|
|
@@ -152,15 +163,15 @@ const ImportForm = observer(({ model }: { model: LGV }) => {
|
|
|
152
163
|
{selectedAsm ? (
|
|
153
164
|
err ? (
|
|
154
165
|
<CloseIcon style={{ color: 'red' }} />
|
|
155
|
-
) :
|
|
166
|
+
) : value ? (
|
|
156
167
|
<RefNameAutocomplete
|
|
157
168
|
fetchResults={fetchResults}
|
|
158
169
|
model={model}
|
|
159
170
|
assemblyName={assemblyError ? undefined : selectedAsm}
|
|
160
|
-
value={
|
|
171
|
+
value={value}
|
|
161
172
|
// note: minWidth 270 accomodates full width of helperText
|
|
162
173
|
minWidth={270}
|
|
163
|
-
|
|
174
|
+
onChange={str => setValue(str)}
|
|
164
175
|
TextFieldProps={{
|
|
165
176
|
variant: 'outlined',
|
|
166
177
|
helperText:
|
|
@@ -181,21 +192,15 @@ const ImportForm = observer(({ model }: { model: LGV }) => {
|
|
|
181
192
|
<Grid item>
|
|
182
193
|
<Button
|
|
183
194
|
type="submit"
|
|
184
|
-
disabled={!
|
|
195
|
+
disabled={!value}
|
|
185
196
|
className={classes.button}
|
|
186
|
-
onClick={() => {
|
|
187
|
-
model.setError(undefined)
|
|
188
|
-
if (selectedRegion) {
|
|
189
|
-
handleSelectedRegion(selectedRegion)
|
|
190
|
-
}
|
|
191
|
-
}}
|
|
192
197
|
variant="contained"
|
|
193
198
|
color="primary"
|
|
194
199
|
>
|
|
195
200
|
Open
|
|
196
201
|
</Button>
|
|
197
202
|
<Button
|
|
198
|
-
disabled={!
|
|
203
|
+
disabled={!value}
|
|
199
204
|
className={classes.button}
|
|
200
205
|
onClick={() => {
|
|
201
206
|
model.setError(undefined)
|
|
@@ -79,7 +79,7 @@ describe('<LinearGenomeView />', () => {
|
|
|
79
79
|
await findByText('Foo Track')
|
|
80
80
|
// test needs to wait until it's updated to display 100 bp in the header to
|
|
81
81
|
// make snapshot pass
|
|
82
|
-
await findByText('
|
|
82
|
+
await findByText('100bp')
|
|
83
83
|
expect(container.firstChild).toMatchSnapshot()
|
|
84
84
|
})
|
|
85
85
|
it('renders two tracks, two regions', async () => {
|
|
@@ -145,7 +145,7 @@ describe('<LinearGenomeView />', () => {
|
|
|
145
145
|
<LinearGenomeView model={model} />,
|
|
146
146
|
)
|
|
147
147
|
await findByText('Foo Track')
|
|
148
|
-
await findByText('
|
|
148
|
+
await findByText('798bp')
|
|
149
149
|
await findAllByTestId('svgfeatures')
|
|
150
150
|
|
|
151
151
|
expect(container.firstChild).toMatchSnapshot()
|
|
@@ -92,16 +92,22 @@ const LinearGenomeView = observer(({ model }: { model: LGV }) => {
|
|
|
92
92
|
<TracksContainer model={model}>
|
|
93
93
|
{!tracks.length ? (
|
|
94
94
|
<Paper variant="outlined" className={classes.note}>
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
95
|
+
{!model.hideNoTracksActive ? (
|
|
96
|
+
<>
|
|
97
|
+
<Typography>No tracks active.</Typography>
|
|
98
|
+
<Button
|
|
99
|
+
variant="contained"
|
|
100
|
+
color="primary"
|
|
101
|
+
onClick={model.activateTrackSelector}
|
|
102
|
+
style={{ zIndex: 1000 }}
|
|
103
|
+
startIcon={<TrackSelectorIcon />}
|
|
104
|
+
>
|
|
105
|
+
Open track selector
|
|
106
|
+
</Button>
|
|
107
|
+
</>
|
|
108
|
+
) : (
|
|
109
|
+
<div style={{ height: '48px' }}></div>
|
|
110
|
+
)}
|
|
105
111
|
</Paper>
|
|
106
112
|
) : (
|
|
107
113
|
tracks.map(track => (
|
|
@@ -5,7 +5,7 @@ import { Instance } from 'mobx-state-tree'
|
|
|
5
5
|
import clsx from 'clsx'
|
|
6
6
|
|
|
7
7
|
import Base1DView, { Base1DViewModel } from '@jbrowse/core/util/Base1DViewModel'
|
|
8
|
-
import { getSession } from '@jbrowse/core/util'
|
|
8
|
+
import { getSession, getTickDisplayStr } from '@jbrowse/core/util'
|
|
9
9
|
import { ContentBlock } from '@jbrowse/core/util/blockTypes'
|
|
10
10
|
import { Assembly } from '@jbrowse/core/assemblyManager/assembly'
|
|
11
11
|
|
|
@@ -51,7 +51,6 @@ const useStyles = makeStyles(theme => {
|
|
|
51
51
|
},
|
|
52
52
|
scaleBarLabel: {
|
|
53
53
|
height: HEADER_OVERVIEW_HEIGHT,
|
|
54
|
-
width: 1,
|
|
55
54
|
position: 'absolute',
|
|
56
55
|
display: 'flex',
|
|
57
56
|
justifyContent: 'center',
|
|
@@ -301,7 +300,7 @@ const OverviewBox = observer(
|
|
|
301
300
|
overview: Base1DViewModel
|
|
302
301
|
}) => {
|
|
303
302
|
const classes = useStyles()
|
|
304
|
-
const { cytobandOffset, showCytobands } = model
|
|
303
|
+
const { cytobandOffset, bpPerPx, showCytobands } = model
|
|
305
304
|
const { start, end, reversed, refName, assemblyName } = block
|
|
306
305
|
const { majorPitch } = chooseGridPitch(scale, 120, 15)
|
|
307
306
|
const { assemblyManager } = getSession(model)
|
|
@@ -354,7 +353,7 @@ const OverviewBox = observer(
|
|
|
354
353
|
color: refNameColor,
|
|
355
354
|
}}
|
|
356
355
|
>
|
|
357
|
-
{tickLabel
|
|
356
|
+
{getTickDisplayStr(tickLabel, bpPerPx)}
|
|
358
357
|
</Typography>
|
|
359
358
|
))
|
|
360
359
|
: null}
|
|
@@ -66,17 +66,19 @@ const MyPopper = function (
|
|
|
66
66
|
|
|
67
67
|
function RefNameAutocomplete({
|
|
68
68
|
model,
|
|
69
|
-
showHelp = true,
|
|
70
69
|
onSelect,
|
|
71
70
|
assemblyName,
|
|
72
71
|
style,
|
|
73
72
|
fetchResults,
|
|
73
|
+
onChange,
|
|
74
74
|
value,
|
|
75
|
+
showHelp = true,
|
|
75
76
|
minWidth = 200,
|
|
76
77
|
TextFieldProps = {},
|
|
77
78
|
}: {
|
|
78
79
|
model: LinearGenomeViewModel
|
|
79
|
-
onSelect
|
|
80
|
+
onSelect?: (region: BaseResult) => void
|
|
81
|
+
onChange?: (val: string) => void
|
|
80
82
|
assemblyName?: string
|
|
81
83
|
value?: string
|
|
82
84
|
fetchResults: (query: string) => Promise<BaseResult[]>
|
|
@@ -171,7 +173,10 @@ function RefNameAutocomplete({
|
|
|
171
173
|
value={inputBoxVal}
|
|
172
174
|
loading={!loaded}
|
|
173
175
|
inputValue={inputValue}
|
|
174
|
-
onInputChange={(event, newInputValue) =>
|
|
176
|
+
onInputChange={(event, newInputValue) => {
|
|
177
|
+
setInputValue(newInputValue)
|
|
178
|
+
onChange?.(newInputValue)
|
|
179
|
+
}}
|
|
175
180
|
loadingText="loading results"
|
|
176
181
|
open={open}
|
|
177
182
|
onOpen={() => setOpen(true)}
|
|
@@ -190,9 +195,9 @@ function RefNameAutocomplete({
|
|
|
190
195
|
|
|
191
196
|
if (typeof selectedOption === 'string') {
|
|
192
197
|
// handles string inputs on keyPress enter
|
|
193
|
-
onSelect(new BaseResult({ label: selectedOption }))
|
|
198
|
+
onSelect?.(new BaseResult({ label: selectedOption }))
|
|
194
199
|
} else {
|
|
195
|
-
onSelect(selectedOption.result)
|
|
200
|
+
onSelect?.(selectedOption.result)
|
|
196
201
|
}
|
|
197
202
|
setInputValue(inputBoxVal)
|
|
198
203
|
}}
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
InterRegionPaddingBlock as InterRegionPaddingBlockComponent,
|
|
15
15
|
} from '../../BaseLinearDisplay/components/Block'
|
|
16
16
|
import { makeTicks } from '../util'
|
|
17
|
+
import { getTickDisplayStr } from '@jbrowse/core/util'
|
|
17
18
|
|
|
18
19
|
type LGV = LinearGenomeViewModel
|
|
19
20
|
|
|
@@ -95,18 +96,14 @@ const RenderedRefNameLabels = observer(({ model }: { model: LGV }) => {
|
|
|
95
96
|
|
|
96
97
|
const RenderedScaleBarLabels = observer(({ model }: { model: LGV }) => {
|
|
97
98
|
const classes = useStyles()
|
|
99
|
+
const { bpPerPx } = model
|
|
98
100
|
|
|
99
101
|
return (
|
|
100
102
|
<>
|
|
101
103
|
{model.staticBlocks.map((block, index) => {
|
|
102
104
|
if (block instanceof ContentBlock) {
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
block.end,
|
|
106
|
-
model.bpPerPx,
|
|
107
|
-
true,
|
|
108
|
-
false,
|
|
109
|
-
)
|
|
105
|
+
const { start, end } = block
|
|
106
|
+
const ticks = makeTicks(start, end, bpPerPx, true, false)
|
|
110
107
|
|
|
111
108
|
return (
|
|
112
109
|
<ContentBlockComponent key={`${block.key}-${index}`} block={block}>
|
|
@@ -116,7 +113,7 @@ const RenderedScaleBarLabels = observer(({ model }: { model: LGV }) => {
|
|
|
116
113
|
(block.reversed
|
|
117
114
|
? block.end - tick.base
|
|
118
115
|
: tick.base - block.start) / model.bpPerPx
|
|
119
|
-
const baseNumber =
|
|
116
|
+
const baseNumber = tick.base + 1
|
|
120
117
|
return (
|
|
121
118
|
<div
|
|
122
119
|
key={tick.base}
|
|
@@ -125,7 +122,7 @@ const RenderedScaleBarLabels = observer(({ model }: { model: LGV }) => {
|
|
|
125
122
|
>
|
|
126
123
|
{baseNumber ? (
|
|
127
124
|
<Typography className={classes.majorTickLabel}>
|
|
128
|
-
{baseNumber}
|
|
125
|
+
{getTickDisplayStr(baseNumber, bpPerPx)}
|
|
129
126
|
</Typography>
|
|
130
127
|
) : null}
|
|
131
128
|
</div>
|
|
@@ -58,19 +58,35 @@ function SearchBox({
|
|
|
58
58
|
)
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
// gets a string as input, or use stored option results from previous query,
|
|
62
|
+
// then re-query and
|
|
63
|
+
// 1) if it has multiple results: pop a dialog
|
|
64
|
+
// 2) if it's a single result navigate to it
|
|
65
|
+
// 3) else assume it's a locstring and navigate to it
|
|
61
66
|
async function handleSelectedRegion(option: BaseResult) {
|
|
62
67
|
let trackId = option.getTrackId()
|
|
63
68
|
let location = option.getLocation()
|
|
64
69
|
const label = option.getLabel()
|
|
70
|
+
const [ref, rest] = location.split(':')
|
|
71
|
+
const allRefs = assembly?.allRefNames || []
|
|
65
72
|
try {
|
|
66
|
-
|
|
67
|
-
|
|
73
|
+
// instead of querying text-index, first:
|
|
74
|
+
// - check if input matches a refName directly
|
|
75
|
+
// - or looks like locString
|
|
76
|
+
// then just navigate as if it were a locString
|
|
77
|
+
if (
|
|
78
|
+
allRefs.includes(location) ||
|
|
79
|
+
(allRefs.includes(ref) &&
|
|
80
|
+
rest !== undefined &&
|
|
81
|
+
!Number.isNaN(parseInt(rest, 10)))
|
|
82
|
+
) {
|
|
83
|
+
model.navToLocString(location, assemblyName)
|
|
68
84
|
} else {
|
|
69
85
|
const results = await fetchResults(label, 'exact')
|
|
70
|
-
if (results
|
|
86
|
+
if (results.length > 1) {
|
|
71
87
|
model.setSearchResults(results, label.toLowerCase())
|
|
72
88
|
return
|
|
73
|
-
} else if (results
|
|
89
|
+
} else if (results.length === 1) {
|
|
74
90
|
location = results[0].getLocation()
|
|
75
91
|
trackId = results[0].getTrackId()
|
|
76
92
|
}
|