react-msaview 8.0.0 → 8.1.0
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/bundle/index.js +99 -99
- package/bundle/index.js.map +3 -3
- package/dist/calculateBlocks.js +5 -3
- package/dist/calculateBlocks.js.map +1 -1
- package/dist/components/Loading.js +2 -8
- package/dist/components/Loading.js.map +1 -1
- package/dist/components/SequenceTextArea.js +0 -1
- package/dist/components/SequenceTextArea.js.map +1 -1
- package/dist/components/Track.js +2 -2
- package/dist/components/Track.js.map +1 -1
- package/dist/components/dialogs/AboutDialog.js +6 -6
- package/dist/components/dialogs/AboutDialog.js.map +1 -1
- package/dist/components/dialogs/AnnotationFileDialog.js +5 -2
- package/dist/components/dialogs/AnnotationFileDialog.js.map +1 -1
- package/dist/components/dialogs/ExportSVGDialog.js +1 -1
- package/dist/components/dialogs/FeatureDialog.js +8 -3
- package/dist/components/dialogs/FeatureDialog.js.map +1 -1
- package/dist/components/header/Header.js +1 -1
- package/dist/components/header/Header.js.map +1 -1
- package/dist/components/header/LoadWarnings.js +3 -19
- package/dist/components/header/LoadWarnings.js.map +1 -1
- package/dist/components/header/settingsMenuItems.js +3 -3
- package/dist/components/header/settingsMenuItems.js.map +1 -1
- package/dist/components/import/ImportForm.js +1 -1
- package/dist/components/import/ImportForm.js.map +1 -1
- package/dist/components/msa/MSACanvas.js +1 -3
- package/dist/components/msa/MSACanvas.js.map +1 -1
- package/dist/components/msa/drawFeatureSpans.d.ts +7 -1
- package/dist/components/msa/drawFeatureSpans.js +58 -23
- package/dist/components/msa/drawFeatureSpans.js.map +1 -1
- package/dist/components/msa/renderBoxFeatureCanvasBlock.js +7 -8
- package/dist/components/msa/renderBoxFeatureCanvasBlock.js.map +1 -1
- package/dist/components/msa/renderMSABlock.js +8 -9
- package/dist/components/msa/renderMSABlock.js.map +1 -1
- package/dist/components/tracks/drawTracks.js +8 -5
- package/dist/components/tracks/drawTracks.js.map +1 -1
- package/dist/components/tree/TreeBranchMenu.js +1 -1
- package/dist/components/tree/TreeBranchMenu.js.map +1 -1
- package/dist/components/tree/TreeCanvas.js +1 -3
- package/dist/components/tree/TreeCanvas.js.map +1 -1
- package/dist/components/tree/TreeCanvasBlock.js +2 -2
- package/dist/components/tree/TreeCanvasBlock.js.map +1 -1
- package/dist/components/tree/TreeNodeMenu.js +1 -1
- package/dist/components/tree/TreeNodeMenu.js.map +1 -1
- package/dist/components/tree/renderTreeCanvas.js +2 -1
- package/dist/components/tree/renderTreeCanvas.js.map +1 -1
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +4 -0
- package/dist/constants.js.map +1 -1
- package/dist/model.d.ts +31 -32
- package/dist/model.js +184 -141
- package/dist/model.js.map +1 -1
- package/dist/svgTestUtil.d.ts +18 -22
- package/dist/types.d.ts +2 -0
- package/dist/useWheelScroll.js +0 -4
- package/dist/useWheelScroll.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -3
- package/src/calculateBlocks.ts +5 -3
- package/src/components/Loading.tsx +2 -30
- package/src/components/SequenceTextArea.tsx +0 -1
- package/src/components/Track.tsx +2 -1
- package/src/components/dialogs/AboutDialog.tsx +35 -6
- package/src/components/dialogs/AnnotationFileDialog.tsx +10 -1
- package/src/components/dialogs/ExportSVGDialog.tsx +1 -1
- package/src/components/dialogs/FeatureDialog.tsx +11 -3
- package/src/components/header/Header.tsx +1 -1
- package/src/components/header/LoadWarnings.tsx +8 -21
- package/src/components/header/settingsMenuItems.ts +3 -3
- package/src/components/import/ImportForm.tsx +3 -5
- package/src/components/msa/MSACanvas.tsx +0 -3
- package/src/components/msa/drawFeatureSpans.ts +80 -25
- package/src/components/msa/renderBoxFeatureCanvasBlock.ts +7 -9
- package/src/components/msa/renderMSABlock.ts +9 -8
- package/src/components/tracks/drawTracks.ts +8 -16
- package/src/components/tree/TreeBranchMenu.tsx +1 -2
- package/src/components/tree/TreeCanvas.tsx +0 -3
- package/src/components/tree/TreeCanvasBlock.tsx +2 -2
- package/src/components/tree/TreeNodeMenu.tsx +1 -2
- package/src/components/tree/renderTreeCanvas.ts +2 -1
- package/src/constants.ts +5 -0
- package/src/model.ts +232 -161
- package/src/types.ts +2 -0
- package/src/useWheelScroll.ts +0 -4
- package/src/version.ts +1 -1
|
@@ -2,7 +2,7 @@ import React from 'react'
|
|
|
2
2
|
|
|
3
3
|
import { ErrorMessage } from '@jbrowse/core/ui'
|
|
4
4
|
import { ErrorBoundary } from '@jbrowse/core/ui/ErrorBoundary'
|
|
5
|
-
import { Button, Typography } from '@mui/material'
|
|
5
|
+
import { Button, CircularProgress, Typography } from '@mui/material'
|
|
6
6
|
import { observer } from 'mobx-react'
|
|
7
7
|
|
|
8
8
|
import MSAView from './MSAView.tsx'
|
|
@@ -16,35 +16,7 @@ const LoadingSpinner = observer(function ({ model }: { model: MsaViewModel }) {
|
|
|
16
16
|
<div
|
|
17
17
|
style={{ display: 'flex', alignItems: 'center', gap: 12, padding: 20 }}
|
|
18
18
|
>
|
|
19
|
-
<
|
|
20
|
-
width="24"
|
|
21
|
-
height="24"
|
|
22
|
-
viewBox="0 0 24 24"
|
|
23
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
24
|
-
>
|
|
25
|
-
<style>
|
|
26
|
-
{`@keyframes spinner { to { transform: rotate(360deg); } }`}
|
|
27
|
-
</style>
|
|
28
|
-
<circle
|
|
29
|
-
cx="12"
|
|
30
|
-
cy="12"
|
|
31
|
-
r="10"
|
|
32
|
-
stroke="#ccc"
|
|
33
|
-
strokeWidth="3"
|
|
34
|
-
fill="none"
|
|
35
|
-
/>
|
|
36
|
-
<path
|
|
37
|
-
d="M12 2a10 10 0 0 1 10 10"
|
|
38
|
-
stroke="#1976d2"
|
|
39
|
-
strokeWidth="3"
|
|
40
|
-
fill="none"
|
|
41
|
-
strokeLinecap="round"
|
|
42
|
-
style={{
|
|
43
|
-
animation: 'spinner 1s linear infinite',
|
|
44
|
-
transformOrigin: 'center',
|
|
45
|
-
}}
|
|
46
|
-
/>
|
|
47
|
-
</svg>
|
|
19
|
+
<CircularProgress size={24} />
|
|
48
20
|
<Typography variant="h6">{status?.msg ?? 'Loading...'}</Typography>
|
|
49
21
|
{status?.onCancel ? (
|
|
50
22
|
<Button
|
package/src/components/Track.tsx
CHANGED
|
@@ -78,6 +78,7 @@ const TrackLabel = observer(function TrackLabel({
|
|
|
78
78
|
</span>
|
|
79
79
|
<IconButton
|
|
80
80
|
className={classes.button}
|
|
81
|
+
aria-label={`${name} track menu`}
|
|
81
82
|
style={{
|
|
82
83
|
width: trackLabelHeight,
|
|
83
84
|
height: trackLabelHeight,
|
|
@@ -105,7 +106,7 @@ const TrackLabel = observer(function TrackLabel({
|
|
|
105
106
|
closeMenu()
|
|
106
107
|
}}
|
|
107
108
|
>
|
|
108
|
-
|
|
109
|
+
Hide track
|
|
109
110
|
</MenuItem>
|
|
110
111
|
<MenuItem
|
|
111
112
|
dense
|
|
@@ -17,16 +17,33 @@ export default function AboutDialog({ onClose }: { onClose: () => void }) {
|
|
|
17
17
|
<DialogContent>
|
|
18
18
|
<Typography>
|
|
19
19
|
MSAView {version} (
|
|
20
|
-
<Link
|
|
20
|
+
<Link
|
|
21
|
+
target="_blank"
|
|
22
|
+
rel="noopener"
|
|
23
|
+
href="https://github.com/gmod/react-msaview"
|
|
24
|
+
>
|
|
25
|
+
Github
|
|
26
|
+
</Link>
|
|
27
|
+
)
|
|
21
28
|
</Typography>
|
|
22
29
|
|
|
23
30
|
<ul>
|
|
24
31
|
<li>
|
|
25
32
|
<Typography>
|
|
26
33
|
We use some color schemes from the{' '}
|
|
27
|
-
<Link
|
|
34
|
+
<Link
|
|
35
|
+
target="_blank"
|
|
36
|
+
rel="noopener"
|
|
37
|
+
href="https://github.com/biotite-dev/biotite"
|
|
38
|
+
>
|
|
39
|
+
biotite
|
|
40
|
+
</Link>{' '}
|
|
28
41
|
project, and their license is reproduced{' '}
|
|
29
|
-
<Link
|
|
42
|
+
<Link
|
|
43
|
+
target="_blank"
|
|
44
|
+
rel="noopener"
|
|
45
|
+
href="https://github.com/biotite-dev/biotite/blob/master/LICENSE.rst"
|
|
46
|
+
>
|
|
30
47
|
here
|
|
31
48
|
</Link>
|
|
32
49
|
</Typography>
|
|
@@ -34,7 +51,11 @@ export default function AboutDialog({ onClose }: { onClose: () => void }) {
|
|
|
34
51
|
<li>
|
|
35
52
|
<Typography>
|
|
36
53
|
See this page for some information on jalview colorings{' '}
|
|
37
|
-
<Link
|
|
54
|
+
<Link
|
|
55
|
+
target="_blank"
|
|
56
|
+
rel="noopener"
|
|
57
|
+
href="https://www.jalview.org/help/html/colourSchemes/"
|
|
58
|
+
>
|
|
38
59
|
here
|
|
39
60
|
</Link>
|
|
40
61
|
</Typography>
|
|
@@ -43,7 +64,11 @@ export default function AboutDialog({ onClose }: { onClose: () => void }) {
|
|
|
43
64
|
<Typography>
|
|
44
65
|
See this page for some info on the clustal, cinema, maeditor, and
|
|
45
66
|
lesk color schemes{' '}
|
|
46
|
-
<Link
|
|
67
|
+
<Link
|
|
68
|
+
target="_blank"
|
|
69
|
+
rel="noopener"
|
|
70
|
+
href="http://www.bioinformatics.nl/~berndb/aacolour.html"
|
|
71
|
+
>
|
|
47
72
|
here
|
|
48
73
|
</Link>
|
|
49
74
|
</Typography>
|
|
@@ -51,7 +76,11 @@ export default function AboutDialog({ onClose }: { onClose: () => void }) {
|
|
|
51
76
|
<li>
|
|
52
77
|
<Typography>
|
|
53
78
|
See this paper about the flower color scheme{' '}
|
|
54
|
-
<Link
|
|
79
|
+
<Link
|
|
80
|
+
target="_blank"
|
|
81
|
+
rel="noopener"
|
|
82
|
+
href="https://www.ncbi.nlm.nih.gov/pmc/articles/PMC7245768/"
|
|
83
|
+
>
|
|
55
84
|
here
|
|
56
85
|
</Link>
|
|
57
86
|
</Typography>
|
|
@@ -126,7 +126,7 @@ const AnnotationFileDialog = observer(function ({
|
|
|
126
126
|
<Button
|
|
127
127
|
variant="contained"
|
|
128
128
|
color="primary"
|
|
129
|
-
disabled={loading}
|
|
129
|
+
disabled={loading || (choice === 'file' ? !file : !url.trim())}
|
|
130
130
|
onClick={() => {
|
|
131
131
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
132
132
|
;(async () => {
|
|
@@ -154,6 +154,15 @@ const AnnotationFileDialog = observer(function ({
|
|
|
154
154
|
>
|
|
155
155
|
Open annotations
|
|
156
156
|
</Button>
|
|
157
|
+
<Button
|
|
158
|
+
variant="contained"
|
|
159
|
+
color="secondary"
|
|
160
|
+
onClick={() => {
|
|
161
|
+
handleClose()
|
|
162
|
+
}}
|
|
163
|
+
>
|
|
164
|
+
Cancel
|
|
165
|
+
</Button>
|
|
157
166
|
</DialogActions>
|
|
158
167
|
</Dialog>
|
|
159
168
|
)
|
|
@@ -47,10 +47,18 @@ const Toggles = observer(function ({ model }: { model: MsaViewModel }) {
|
|
|
47
47
|
})
|
|
48
48
|
|
|
49
49
|
const FeatureTable = observer(function ({ model }: { model: MsaViewModel }) {
|
|
50
|
-
const { annotationTypes, annotations, fillPalette } = model
|
|
50
|
+
const { annotationTypes, annotations, featureColors, fillPalette } = model
|
|
51
51
|
const counts = new Map<string, number>()
|
|
52
|
+
const swatches = new Map<string, string>()
|
|
52
53
|
for (const annot of annotations) {
|
|
53
|
-
|
|
54
|
+
const { accession } = annot
|
|
55
|
+
counts.set(accession, (counts.get(accession) ?? 0) + 1)
|
|
56
|
+
if (!swatches.has(accession)) {
|
|
57
|
+
swatches.set(
|
|
58
|
+
accession,
|
|
59
|
+
featureColors.get(annot)?.fill ?? fillPalette[accession]!,
|
|
60
|
+
)
|
|
61
|
+
}
|
|
54
62
|
}
|
|
55
63
|
return (
|
|
56
64
|
<>
|
|
@@ -86,7 +94,7 @@ const FeatureTable = observer(function ({ model }: { model: MsaViewModel }) {
|
|
|
86
94
|
style={{
|
|
87
95
|
width: 20,
|
|
88
96
|
height: 20,
|
|
89
|
-
background:
|
|
97
|
+
background: swatches.get(accession),
|
|
90
98
|
}}
|
|
91
99
|
/>
|
|
92
100
|
</TableCell>
|
|
@@ -34,7 +34,7 @@ const Header = observer(function ({ model }: { model: MsaViewModel }) {
|
|
|
34
34
|
[model],
|
|
35
35
|
)
|
|
36
36
|
return (
|
|
37
|
-
<div ref={ref} style={{ display: 'flex' }}>
|
|
37
|
+
<div ref={ref} style={{ display: 'flex', flexWrap: 'wrap' }}>
|
|
38
38
|
<FileMenu model={model} />
|
|
39
39
|
<ColorSchemeMenu model={model} />
|
|
40
40
|
<TreeSettingsMenu model={model} />
|
|
@@ -1,46 +1,33 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
|
|
3
|
-
import { makeStyles } from '@jbrowse/core/util/tss-react'
|
|
4
3
|
import Warning from '@mui/icons-material/Warning'
|
|
5
|
-
import {
|
|
4
|
+
import { Button, Tooltip } from '@mui/material'
|
|
6
5
|
import { observer } from 'mobx-react'
|
|
7
6
|
|
|
8
7
|
import type { MsaViewModel } from '../../model.ts'
|
|
9
8
|
|
|
10
|
-
const useStyles = makeStyles()(theme => ({
|
|
11
|
-
warning: {
|
|
12
|
-
display: 'flex',
|
|
13
|
-
alignItems: 'center',
|
|
14
|
-
gap: 4,
|
|
15
|
-
margin: 'auto',
|
|
16
|
-
marginLeft: 10,
|
|
17
|
-
whiteSpace: 'nowrap',
|
|
18
|
-
color: theme.palette.warning.main,
|
|
19
|
-
cursor: 'pointer',
|
|
20
|
-
},
|
|
21
|
-
}))
|
|
22
|
-
|
|
23
9
|
// A failed optional layer, such as a 404 on the annotations or row metadata
|
|
24
10
|
// that is not JSON, shows here as a dismissible warning. Setting `error` would
|
|
25
11
|
// clear dataInitialized and hide the loaded alignment.
|
|
26
12
|
const LoadWarnings = observer(function ({ model }: { model: MsaViewModel }) {
|
|
27
13
|
const { warnings } = model
|
|
28
|
-
const { classes } = useStyles()
|
|
29
14
|
if (warnings.length === 0) {
|
|
30
15
|
return null
|
|
31
16
|
}
|
|
32
17
|
return (
|
|
33
18
|
<Tooltip title={`${warnings.join(' ')} (click to dismiss)`}>
|
|
34
|
-
<
|
|
35
|
-
|
|
36
|
-
|
|
19
|
+
<Button
|
|
20
|
+
aria-label="Dismiss warnings"
|
|
21
|
+
color="warning"
|
|
22
|
+
size="small"
|
|
23
|
+
startIcon={<Warning fontSize="small" />}
|
|
24
|
+
style={{ margin: 'auto', marginLeft: 10, whiteSpace: 'nowrap' }}
|
|
37
25
|
onClick={() => {
|
|
38
26
|
model.clearWarnings()
|
|
39
27
|
}}
|
|
40
28
|
>
|
|
41
|
-
<Warning fontSize="small" />
|
|
42
29
|
{warnings.length > 1 ? `${warnings.length} warnings` : 'Warning'}
|
|
43
|
-
</
|
|
30
|
+
</Button>
|
|
44
31
|
</Tooltip>
|
|
45
32
|
)
|
|
46
33
|
})
|
|
@@ -12,7 +12,7 @@ function toggle(label: string, checked: boolean, set: (arg: boolean) => void) {
|
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
// the only place in the UI to turn a
|
|
15
|
+
// the only place in the UI to turn a hidden track back on
|
|
16
16
|
function tracksSubMenu(model: MsaViewModel): MenuItem[] {
|
|
17
17
|
const shown = new Set(model.turnedOnTracks.map(t => t.model.id))
|
|
18
18
|
return model.tracks.map(({ model: { id, name } }) =>
|
|
@@ -38,10 +38,10 @@ export function msaSettingsMenuItems(model: MsaViewModel): MenuItem[] {
|
|
|
38
38
|
model.setDrawMsaLetters(arg)
|
|
39
39
|
}),
|
|
40
40
|
// inverted: bgColor colors the tile, and off colors the letter
|
|
41
|
-
toggle('Color letters
|
|
41
|
+
toggle('Color letters, not cells', !bgColor, arg => {
|
|
42
42
|
model.setBgColor(!arg)
|
|
43
43
|
}),
|
|
44
|
-
toggle('
|
|
44
|
+
toggle('Hide gappy columns', hideGaps, arg => {
|
|
45
45
|
model.setHideGaps(arg)
|
|
46
46
|
}),
|
|
47
47
|
]
|
|
@@ -24,11 +24,9 @@ const ImportForm = observer(function ({ model }: { model: MsaViewModel }) {
|
|
|
24
24
|
Open an MSA file (FASTA, Stockholm, Clustal, A3M or EMF format) and/or
|
|
25
25
|
a tree file (Newick format).
|
|
26
26
|
</Typography>
|
|
27
|
-
<Typography color="
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
also and this is fine, and opening a separate tree file is not
|
|
31
|
-
required.
|
|
27
|
+
<Typography color="text.secondary">
|
|
28
|
+
An MSA alone or a tree alone is enough, and a Stockholm file with an
|
|
29
|
+
embedded tree needs no separate tree file.
|
|
32
30
|
</Typography>
|
|
33
31
|
</div>
|
|
34
32
|
|
|
@@ -54,9 +54,6 @@ const MSACanvas = observer(function ({ model }: { model: MsaViewModel }) {
|
|
|
54
54
|
data-testid="msa_canvas"
|
|
55
55
|
onMouseDown={onMouseDown}
|
|
56
56
|
onMouseUp={onMouseUp}
|
|
57
|
-
onMouseLeave={event => {
|
|
58
|
-
event.preventDefault()
|
|
59
|
-
}}
|
|
60
57
|
style={{
|
|
61
58
|
position: 'relative',
|
|
62
59
|
height,
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { minFeatureLabelHeight } from '../../constants.ts'
|
|
2
|
+
|
|
1
3
|
import type { HierarchyNode } from '../../hierarchy.ts'
|
|
2
4
|
import type { Annotation, NodeWithIdsAndLength } from '../../types.ts'
|
|
3
5
|
import type { RenderCtx } from '../renderCtx.ts'
|
|
@@ -25,8 +27,14 @@ export interface SpanLayout {
|
|
|
25
27
|
height: (laneCount: number) => number
|
|
26
28
|
/** the band's top edge, from its row baseline, its lane and its height */
|
|
27
29
|
top: (y: number, lane: number, height: number) => number
|
|
28
|
-
/** how far
|
|
30
|
+
/** how far back from the feature's end the head starts */
|
|
29
31
|
headLength: (height: number) => number
|
|
32
|
+
/**
|
|
33
|
+
* how far the head rises above the band. A bar too thin to taper reads its
|
|
34
|
+
* direction off a head taller than itself, and the bar sits on the row's
|
|
35
|
+
* bottom edge, so the room is above.
|
|
36
|
+
*/
|
|
37
|
+
headRise?: (height: number) => number
|
|
30
38
|
}
|
|
31
39
|
|
|
32
40
|
/** the rows of `bandsByRow` that `leaves` covers, in display order */
|
|
@@ -76,9 +84,26 @@ export function drawFeatureSpans<T extends SpanBand>({
|
|
|
76
84
|
xMin: number
|
|
77
85
|
xMax: number
|
|
78
86
|
}) {
|
|
87
|
+
// one row's spans are drawn before any of its labels. Bands are ordered
|
|
88
|
+
// longest-first so a nested one lands on top, and drawing each label with its
|
|
89
|
+
// own span put the child's box over the parent's text: `S` covered `RBD`, and
|
|
90
|
+
// `RBD` read as `RB`. Reused across rows rather than allocated per row.
|
|
91
|
+
const placed: {
|
|
92
|
+
band: T
|
|
93
|
+
xStart: number
|
|
94
|
+
w: number
|
|
95
|
+
t: number
|
|
96
|
+
head: number
|
|
97
|
+
fill: string
|
|
98
|
+
}[] = []
|
|
99
|
+
|
|
79
100
|
for (const { y, bands } of rows) {
|
|
80
101
|
const h = layout.height(bands[0]!.laneCount)
|
|
81
102
|
const headLen = layout.headLength(h)
|
|
103
|
+
const rise = layout.headRise?.(h) ?? 0
|
|
104
|
+
const labelled = labelOf !== undefined && h >= minFeatureLabelHeight
|
|
105
|
+
placed.length = 0
|
|
106
|
+
|
|
82
107
|
for (const band of bands) {
|
|
83
108
|
const [xStart, xEnd] = xOf(band)
|
|
84
109
|
const w = xEnd - xStart
|
|
@@ -90,21 +115,33 @@ export function drawFeatureSpans<T extends SpanBand>({
|
|
|
90
115
|
ctx.fillStyle = fill
|
|
91
116
|
ctx.strokeStyle = stroke
|
|
92
117
|
const { strand } = band.annotation
|
|
118
|
+
const head = strand === undefined ? 0 : Math.min(headLen, w)
|
|
93
119
|
if (strand === undefined) {
|
|
94
120
|
ctx.fillRect(xStart, t, w, h)
|
|
95
121
|
ctx.strokeRect(xStart, t, w, h)
|
|
96
122
|
} else {
|
|
97
|
-
drawGeneArrow({ ctx, x: xStart, t, w, h,
|
|
123
|
+
drawGeneArrow({ ctx, x: xStart, t, w, h, head, rise, strand })
|
|
98
124
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
125
|
+
if (labelled) {
|
|
126
|
+
placed.push({ band, xStart, w, t, head, fill })
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (placed.length > 0) {
|
|
131
|
+
ctx.font = `${Math.min(h - 2, 11)}px sans-serif`
|
|
132
|
+
ctx.textAlign = 'center'
|
|
133
|
+
ctx.textBaseline = 'middle'
|
|
134
|
+
for (const { band, xStart, w, t, head, fill } of placed) {
|
|
135
|
+
// asking for the label here rather than in the pass above keeps the
|
|
136
|
+
// once-per-block segment numbering in drawing order
|
|
137
|
+
const label = labelOf!(band)
|
|
138
|
+
// the head narrows to a point, so only about half of it holds text
|
|
139
|
+
if (
|
|
140
|
+
label !== undefined &&
|
|
141
|
+
ctx.measureText(label).width + 2 <= w - head / 2
|
|
142
|
+
) {
|
|
104
143
|
labelDrawn?.(band)
|
|
105
144
|
ctx.fillStyle = contrastText(fill)
|
|
106
|
-
ctx.textAlign = 'center'
|
|
107
|
-
ctx.textBaseline = 'middle'
|
|
108
145
|
ctx.fillText(label, xStart + w / 2, t + h / 2)
|
|
109
146
|
}
|
|
110
147
|
}
|
|
@@ -112,19 +149,24 @@ export function drawFeatureSpans<T extends SpanBand>({
|
|
|
112
149
|
}
|
|
113
150
|
}
|
|
114
151
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
152
|
+
/**
|
|
153
|
+
* A gene arrow, drawn the way gggenes and gggenomes draw one: the head is the
|
|
154
|
+
* last `head` pixels *of* the feature, tapering to a point at its end, and a
|
|
155
|
+
* feature shorter than the head is all head. The glyph therefore covers its
|
|
156
|
+
* start..end span and nothing else, so adjacent genes butt together instead of
|
|
157
|
+
* biting triangles out of each other, a neighbour never draws over a label, and
|
|
158
|
+
* a gene 20 columns wide reads as 20 columns wide.
|
|
159
|
+
*
|
|
160
|
+
* `rise` lifts the head above the band, for a bar too thin to taper.
|
|
161
|
+
*/
|
|
121
162
|
function drawGeneArrow({
|
|
122
163
|
ctx,
|
|
123
164
|
x,
|
|
124
165
|
t,
|
|
125
166
|
w,
|
|
126
167
|
h,
|
|
127
|
-
|
|
168
|
+
head,
|
|
169
|
+
rise,
|
|
128
170
|
strand,
|
|
129
171
|
}: {
|
|
130
172
|
ctx: RenderCtx
|
|
@@ -132,18 +174,31 @@ function drawGeneArrow({
|
|
|
132
174
|
t: number
|
|
133
175
|
w: number
|
|
134
176
|
h: number
|
|
135
|
-
|
|
177
|
+
head: number
|
|
178
|
+
rise: number
|
|
136
179
|
strand: number
|
|
137
180
|
}) {
|
|
138
|
-
const
|
|
139
|
-
const
|
|
140
|
-
const
|
|
181
|
+
const tip = strand > 0 ? x + w : x
|
|
182
|
+
const tail = strand > 0 ? x : x + w
|
|
183
|
+
const flange = strand > 0 ? tip - head : tip + head
|
|
184
|
+
const bottom = t + h
|
|
185
|
+
const crown = t - rise
|
|
186
|
+
const point = (crown + bottom) / 2
|
|
141
187
|
ctx.beginPath()
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
188
|
+
if (head >= w) {
|
|
189
|
+
ctx.moveTo(tail, crown)
|
|
190
|
+
ctx.lineTo(tip, point)
|
|
191
|
+
ctx.lineTo(tail, bottom)
|
|
192
|
+
} else {
|
|
193
|
+
ctx.moveTo(tail, t)
|
|
194
|
+
ctx.lineTo(flange, t)
|
|
195
|
+
if (rise > 0) {
|
|
196
|
+
ctx.lineTo(flange, crown)
|
|
197
|
+
}
|
|
198
|
+
ctx.lineTo(tip, point)
|
|
199
|
+
ctx.lineTo(flange, bottom)
|
|
200
|
+
ctx.lineTo(tail, bottom)
|
|
201
|
+
}
|
|
147
202
|
ctx.closePath()
|
|
148
203
|
ctx.fill()
|
|
149
204
|
ctx.stroke()
|
|
@@ -60,16 +60,12 @@ export function renderBoxFeatureCanvasBlock({
|
|
|
60
60
|
// out); when letters show, the alternating shades alone mark the boundaries
|
|
61
61
|
// and a number would collide with the sequence. A featureLabel encoding names
|
|
62
62
|
// what every span carries instead
|
|
63
|
-
const drawSegmentLabels =
|
|
64
|
-
!featureLabels && !showMsaLetters && !subFeatureRows && barHeight >= 9
|
|
63
|
+
const drawSegmentLabels = !featureLabels && !showMsaLetters && !subFeatureRows
|
|
65
64
|
// a segment (exon) number labels its band once per block, on the topmost
|
|
66
65
|
// visible row carrying that segment, so it reads as a column header for the
|
|
67
66
|
// whole band. Keyed by accession rather than drawn on row 0, because the rows
|
|
68
67
|
// carrying the gene model are often not the first ones in the alignment
|
|
69
68
|
const labelled = drawSegmentLabels ? new Set<string>() : undefined
|
|
70
|
-
// gene arrow heads stick out up to a row height past the band, so pad the
|
|
71
|
-
// cull window enough that a band just outside the block still draws its head
|
|
72
|
-
const cull = rowHeight + colWidth
|
|
73
69
|
|
|
74
70
|
drawFeatureSpans<DomainBand>({
|
|
75
71
|
ctx,
|
|
@@ -88,9 +84,11 @@ export function renderBoxFeatureCanvasBlock({
|
|
|
88
84
|
y -
|
|
89
85
|
rowHeight +
|
|
90
86
|
(subFeatureRows ? lane * h : domainUnderline ? rowHeight - h : 0),
|
|
91
|
-
// the head keeps its full size on an underline bar, since a head as short
|
|
92
|
-
// as the bar reads as a nub rather than as a direction
|
|
93
87
|
headLength: h => (domainUnderline ? rowHeight / 2 : h),
|
|
88
|
+
// a head only as tall as an underline bar tapers over ten pixels of
|
|
89
|
+
// three, which reads as nothing; it takes the lower two fifths of the
|
|
90
|
+
// row instead, leaving the letters above it
|
|
91
|
+
headRise: h => (domainUnderline ? Math.max(0, rowHeight * 0.4 - h) : 0),
|
|
94
92
|
},
|
|
95
93
|
xOf: band => [band.startCol * colWidth, band.endCol * colWidth],
|
|
96
94
|
colors: featureColors,
|
|
@@ -101,7 +99,7 @@ export function renderBoxFeatureCanvasBlock({
|
|
|
101
99
|
: undefined),
|
|
102
100
|
labelDrawn: band => labelled?.add(band.annotation.accession),
|
|
103
101
|
contrastText: contrastTextFn(theme),
|
|
104
|
-
xMin: offsetX
|
|
105
|
-
xMax: offsetX + bx
|
|
102
|
+
xMin: offsetX,
|
|
103
|
+
xMax: offsetX + bx,
|
|
106
104
|
})
|
|
107
105
|
}
|
|
@@ -113,19 +113,18 @@ function drawTilesAndText({
|
|
|
113
113
|
columns,
|
|
114
114
|
colWidth,
|
|
115
115
|
rowHeight,
|
|
116
|
+
fontSize,
|
|
116
117
|
relativeTo,
|
|
117
118
|
showMsaLetters,
|
|
118
119
|
subFeatureRows,
|
|
119
120
|
domainUnderline,
|
|
120
121
|
domainBandsByStart,
|
|
122
|
+
featureColors,
|
|
121
123
|
} = model
|
|
122
124
|
|
|
123
125
|
const tiles = drawTiles && bgColor
|
|
124
126
|
const paintTiles = tiles && !rasterTiles
|
|
125
127
|
if (paintTiles || showMsaLetters) {
|
|
126
|
-
// a letter takes its color from the cell it lands on, so the scheme lookup
|
|
127
|
-
// is only waste when nothing draws letters
|
|
128
|
-
const needsColor = paintTiles || showMsaLetters
|
|
129
128
|
const tileColor = tileColorFn(model)
|
|
130
129
|
// Domain fills come from a categorical palette with no fixed lightness, and
|
|
131
130
|
// a dynamic scheme colors a cell from its column, so a readable letter color
|
|
@@ -133,8 +132,10 @@ function drawTilesAndText({
|
|
|
133
132
|
const contrastText = contrastTextFn(theme)
|
|
134
133
|
const offsetXAligned = xStart * colWidth
|
|
135
134
|
const halfColWidth = colWidth / 2
|
|
136
|
-
//
|
|
137
|
-
|
|
135
|
+
// the context is translated by rowHeight/2, so `y` is the row's bottom
|
|
136
|
+
// edge and the baseline lands fontSize/4 below the row center, where the
|
|
137
|
+
// tree puts its tip labels
|
|
138
|
+
const baselineOffset = rowHeight / 2 - fontSize / 4
|
|
138
139
|
// with the tiles coming from the domain overlay instead, letters sit either
|
|
139
140
|
// on a domain box or on the plain background; sub-row layout stacks the boxes
|
|
140
141
|
// above the letters and letter-color mode shrinks them to a bar under the
|
|
@@ -148,7 +149,7 @@ function drawTilesAndText({
|
|
|
148
149
|
const str = columns.get(name)?.slice(xStart, xEnd)
|
|
149
150
|
if (str) {
|
|
150
151
|
const tileY = y - rowHeight
|
|
151
|
-
const textY = y -
|
|
152
|
+
const textY = y - baselineOffset
|
|
152
153
|
const bandAt = domainBandCursor(
|
|
153
154
|
overDomains ? domainBandsByStart.get(name) : undefined,
|
|
154
155
|
)
|
|
@@ -159,7 +160,7 @@ function drawTilesAndText({
|
|
|
159
160
|
const x = j * colWidth + offsetXAligned
|
|
160
161
|
const isMatchingReference =
|
|
161
162
|
referenceSeq && name !== relativeTo && letter === referenceSeq[j]
|
|
162
|
-
const color =
|
|
163
|
+
const color = tileColor(col, letter)
|
|
163
164
|
|
|
164
165
|
if (paintTiles) {
|
|
165
166
|
ctx.fillStyle = isMatchingReference
|
|
@@ -172,7 +173,7 @@ function drawTilesAndText({
|
|
|
172
173
|
const covering = bandAt(col)
|
|
173
174
|
ctx.fillStyle = covering
|
|
174
175
|
? // on top of a domain box: contrast against the box fill
|
|
175
|
-
contrastText(
|
|
176
|
+
contrastText(featureColors.get(covering.annotation)?.fill)
|
|
176
177
|
: isMatchingReference
|
|
177
178
|
? // the dot sits on the faint reference-match wash, which is
|
|
178
179
|
// the theme's background with a little of its text color in it
|
|
@@ -309,18 +309,9 @@ export function drawTrackBlock({
|
|
|
309
309
|
blockSizeXOverride?: number
|
|
310
310
|
highResScaleFactorOverride?: number
|
|
311
311
|
}) {
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
colWidth,
|
|
316
|
-
colStats,
|
|
317
|
-
colorScheme: modelColorScheme,
|
|
318
|
-
fontSize,
|
|
319
|
-
rowHeight,
|
|
320
|
-
showMsaLetters,
|
|
321
|
-
alphabetMaxBits,
|
|
322
|
-
highResScaleFactor,
|
|
323
|
-
} = model
|
|
312
|
+
// an autorun tracks every model read here, so each kind reads only what it
|
|
313
|
+
// draws and a vertical zoom leaves the bar and arc canvases alone
|
|
314
|
+
const { blockSize, colWidth, highResScaleFactor } = model
|
|
324
315
|
const {
|
|
325
316
|
id,
|
|
326
317
|
kind,
|
|
@@ -367,9 +358,9 @@ export function drawTrackBlock({
|
|
|
367
358
|
case 'logo': {
|
|
368
359
|
drawSequenceLogo({
|
|
369
360
|
ctx,
|
|
370
|
-
colStats,
|
|
371
|
-
colorScheme:
|
|
372
|
-
maxBits: alphabetMaxBits,
|
|
361
|
+
colStats: model.colStats,
|
|
362
|
+
colorScheme: model.colorScheme,
|
|
363
|
+
maxBits: model.alphabetMaxBits,
|
|
373
364
|
textColor,
|
|
374
365
|
colWidth,
|
|
375
366
|
trackHeight,
|
|
@@ -399,13 +390,14 @@ export function drawTrackBlock({
|
|
|
399
390
|
break
|
|
400
391
|
}
|
|
401
392
|
case 'text': {
|
|
393
|
+
const { bgColor, fontSize, rowHeight, showMsaLetters } = model
|
|
402
394
|
ctx.textAlign = 'center'
|
|
403
395
|
ctx.textBaseline = 'middle'
|
|
404
396
|
setFontSize(ctx, fontSize)
|
|
405
397
|
drawTextTrackContent({
|
|
406
398
|
ctx,
|
|
407
399
|
data,
|
|
408
|
-
colorScheme: customColorScheme ??
|
|
400
|
+
colorScheme: customColorScheme ?? model.colorScheme,
|
|
409
401
|
contrastText: contrastTextFn(theme),
|
|
410
402
|
bgColor,
|
|
411
403
|
drawLetters: showMsaLetters,
|