react-msaview 5.4.1 → 5.5.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/LICENSE +21 -0
- package/bundle/index.js +465 -138
- package/bundle/index.js.map +7 -1
- package/dist/colorSchemes.js +11 -7
- package/dist/colorSchemes.js.map +1 -1
- package/dist/components/MSAViewer.d.ts +7 -1
- package/dist/components/MSAViewer.js +10 -7
- package/dist/components/MSAViewer.js.map +1 -1
- package/dist/components/Track.js +8 -5
- package/dist/components/Track.js.map +1 -1
- package/dist/components/dialogs/FeatureDialog.js +27 -23
- package/dist/components/dialogs/FeatureDialog.js.map +1 -1
- package/dist/components/header/MultiAlignmentSelector.js +0 -0
- package/dist/components/header/MultiAlignmentSelector.js.map +1 -1
- package/dist/components/msa/DomainLegend.d.ts +6 -0
- package/dist/components/msa/DomainLegend.js +41 -0
- package/dist/components/msa/DomainLegend.js.map +1 -0
- package/dist/components/msa/MSACanvasBlock.js +17 -5
- package/dist/components/msa/MSACanvasBlock.js.map +1 -1
- package/dist/components/msa/MSAPanel.js +3 -1
- package/dist/components/msa/MSAPanel.js.map +1 -1
- package/dist/components/msa/renderBoxFeatureCanvasBlock.js +58 -14
- package/dist/components/msa/renderBoxFeatureCanvasBlock.js.map +1 -1
- package/dist/components/msa/renderMSABlock.js +3 -3
- package/dist/components/msa/renderMSABlock.js.map +1 -1
- package/dist/components/msa/renderMSAMouseover.js +32 -4
- package/dist/components/msa/renderMSAMouseover.js.map +1 -1
- package/dist/components/renderCtx.d.ts +1 -1
- package/dist/components/tracks/renderTracksSvg.js +3 -3
- package/dist/components/tracks/renderTracksSvg.js.map +1 -1
- package/dist/constants.d.ts +3 -0
- package/dist/constants.js +16 -0
- package/dist/constants.js.map +1 -1
- package/dist/createPaletteMap.d.ts +5 -2
- package/dist/createPaletteMap.js +15 -5
- package/dist/createPaletteMap.js.map +1 -1
- package/dist/launchInterProScan.d.ts +2 -0
- package/dist/launchInterProScan.js.map +1 -1
- package/dist/model.d.ts +96 -1
- package/dist/model.js +91 -3
- package/dist/model.js.map +1 -1
- package/dist/renderToSvg.js +104 -74
- package/dist/renderToSvg.js.map +1 -1
- package/dist/{webpack.js → umd.js} +1 -1
- package/dist/umd.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +7 -6
- package/src/colorSchemes.ts +12 -7
- package/src/components/MSAViewer.tsx +30 -18
- package/src/components/Track.tsx +8 -5
- package/src/components/dialogs/FeatureDialog.tsx +62 -48
- package/src/components/header/MultiAlignmentSelector.tsx +0 -0
- package/src/components/msa/DomainLegend.tsx +74 -0
- package/src/components/msa/MSACanvasBlock.tsx +22 -4
- package/src/components/msa/MSAPanel.tsx +2 -0
- package/src/components/msa/renderBoxFeatureCanvasBlock.ts +73 -17
- package/src/components/msa/renderMSABlock.ts +3 -3
- package/src/components/msa/renderMSAMouseover.ts +32 -4
- package/src/components/renderCtx.ts +1 -0
- package/src/components/tracks/renderTracksSvg.ts +3 -3
- package/src/constants.ts +18 -0
- package/src/createPaletteMap.ts +18 -5
- package/src/launchInterProScan.ts +3 -1
- package/src/model.ts +105 -2
- package/src/renderDomainLegendSvg.test.tsx +134 -0
- package/src/renderToSvg.tsx +210 -147
- package/src/segmentOverlay.test.ts +70 -0
- package/src/version.ts +1 -1
- package/bundle/index.js.LICENSE.txt +0 -100
- package/dist/components/util.d.ts +0 -1
- package/dist/components/util.js +0 -7
- package/dist/components/util.js.map +0 -1
- package/dist/webpack.js.map +0 -1
- package/src/components/util.ts +0 -9
- /package/dist/{webpack.d.ts → umd.d.ts} +0 -0
- /package/src/{webpack.ts → umd.ts} +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useEffect,
|
|
1
|
+
import React, { useEffect, useState } from 'react'
|
|
2
2
|
|
|
3
3
|
import { createJBrowseTheme } from '@jbrowse/core/ui/theme'
|
|
4
4
|
import useMeasure from '@jbrowse/core/util/useMeasure'
|
|
@@ -9,6 +9,8 @@ import Loading from './Loading.tsx'
|
|
|
9
9
|
|
|
10
10
|
import type { FileLocation as FileLocationType } from '@jbrowse/core/util/types'
|
|
11
11
|
|
|
12
|
+
const theme = createJBrowseTheme()
|
|
13
|
+
|
|
12
14
|
interface MSAViewerProps {
|
|
13
15
|
msa?: string
|
|
14
16
|
tree?: string
|
|
@@ -18,6 +20,12 @@ interface MSAViewerProps {
|
|
|
18
20
|
gffFilehandle?: FileLocationType
|
|
19
21
|
colorScheme?: string
|
|
20
22
|
height?: number
|
|
23
|
+
/** initial per-column pixel width (zoom level) */
|
|
24
|
+
colWidth?: number
|
|
25
|
+
/** initial per-row pixel height */
|
|
26
|
+
rowHeight?: number
|
|
27
|
+
/** alignment columns (0-based) to highlight with a persistent overlay */
|
|
28
|
+
highlightColumns?: number[]
|
|
21
29
|
/** row name to diff every other row against (matches render as ".") */
|
|
22
30
|
relativeTo?: string
|
|
23
31
|
}
|
|
@@ -31,25 +39,29 @@ export default function MSAViewer({
|
|
|
31
39
|
gffFilehandle,
|
|
32
40
|
colorScheme,
|
|
33
41
|
height,
|
|
42
|
+
colWidth,
|
|
43
|
+
rowHeight,
|
|
44
|
+
highlightColumns,
|
|
34
45
|
relativeTo,
|
|
35
46
|
}: MSAViewerProps) {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}),
|
|
51
|
-
|
|
52
|
-
|
|
47
|
+
// lazy initializer: the model is created exactly once from the initial props
|
|
48
|
+
// (a stable MST instance — not a value safe to recompute, so not useMemo)
|
|
49
|
+
const [model] = useState(() =>
|
|
50
|
+
MSAModelF().create({
|
|
51
|
+
type: 'MsaView',
|
|
52
|
+
...(msa || tree || gff
|
|
53
|
+
? { data: { msa: msa ?? '', tree: tree ?? '', gff } }
|
|
54
|
+
: {}),
|
|
55
|
+
...(msaFilehandle ? { msaFilehandle } : {}),
|
|
56
|
+
...(treeFilehandle ? { treeFilehandle } : {}),
|
|
57
|
+
...(gffFilehandle ? { gffFilehandle } : {}),
|
|
58
|
+
...(colorScheme ? { colorSchemeName: colorScheme } : {}),
|
|
59
|
+
...(height ? { height } : {}),
|
|
60
|
+
...(colWidth ? { colWidth } : {}),
|
|
61
|
+
...(rowHeight ? { rowHeight } : {}),
|
|
62
|
+
...(highlightColumns ? { highlightColumns } : {}),
|
|
63
|
+
...(relativeTo ? { relativeTo } : {}),
|
|
64
|
+
}),
|
|
53
65
|
)
|
|
54
66
|
|
|
55
67
|
const [ref, { width }] = useMeasure()
|
package/src/components/Track.tsx
CHANGED
|
@@ -26,12 +26,15 @@ const TrackLabel = observer(function TrackLabel({
|
|
|
26
26
|
track: BasicTrack
|
|
27
27
|
}) {
|
|
28
28
|
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement>()
|
|
29
|
-
const
|
|
29
|
+
const closeMenu = () => {
|
|
30
|
+
setAnchorEl(undefined)
|
|
31
|
+
}
|
|
32
|
+
const { drawLabels, fontSize, treeAreaWidth: width } = model
|
|
30
33
|
const {
|
|
31
34
|
model: { name, height },
|
|
32
35
|
} = track
|
|
33
36
|
const { classes } = useStyles()
|
|
34
|
-
const trackLabelHeight = Math.min(height,
|
|
37
|
+
const trackLabelHeight = Math.min(height, fontSize)
|
|
35
38
|
|
|
36
39
|
return (
|
|
37
40
|
<div
|
|
@@ -62,14 +65,14 @@ const TrackLabel = observer(function TrackLabel({
|
|
|
62
65
|
transitionDuration={0}
|
|
63
66
|
open
|
|
64
67
|
onClose={() => {
|
|
65
|
-
|
|
68
|
+
closeMenu()
|
|
66
69
|
}}
|
|
67
70
|
>
|
|
68
71
|
<MenuItem
|
|
69
72
|
dense
|
|
70
73
|
onClick={() => {
|
|
71
74
|
model.toggleTrack(track.model.id)
|
|
72
|
-
|
|
75
|
+
closeMenu()
|
|
73
76
|
}}
|
|
74
77
|
>
|
|
75
78
|
Close
|
|
@@ -81,7 +84,7 @@ const TrackLabel = observer(function TrackLabel({
|
|
|
81
84
|
TrackInfoDialog,
|
|
82
85
|
{ onClose, model: track.model },
|
|
83
86
|
])
|
|
84
|
-
|
|
87
|
+
closeMenu()
|
|
85
88
|
}}
|
|
86
89
|
>
|
|
87
90
|
Get info
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
|
|
3
3
|
import { Dialog } from '@jbrowse/core/ui'
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
Button,
|
|
6
|
+
Checkbox,
|
|
7
|
+
DialogContent,
|
|
8
|
+
Table,
|
|
9
|
+
TableBody,
|
|
10
|
+
TableCell,
|
|
11
|
+
TableHead,
|
|
12
|
+
TableRow,
|
|
13
|
+
} from '@mui/material'
|
|
5
14
|
import { observer } from 'mobx-react'
|
|
6
15
|
|
|
7
|
-
import { getPalette } from '../../ggplotPalettes.ts'
|
|
8
|
-
|
|
9
16
|
import type { MsaViewModel } from '../../model.ts'
|
|
10
17
|
|
|
11
18
|
const Toggles = observer(function ({ model }: { model: MsaViewModel }) {
|
|
@@ -35,53 +42,60 @@ const Toggles = observer(function ({ model }: { model: MsaViewModel }) {
|
|
|
35
42
|
)
|
|
36
43
|
})
|
|
37
44
|
|
|
38
|
-
const
|
|
39
|
-
const { tidyInterProAnnotationTypes,
|
|
40
|
-
|
|
41
|
-
const
|
|
45
|
+
const FeatureTable = observer(function ({ model }: { model: MsaViewModel }) {
|
|
46
|
+
const { tidyInterProAnnotationTypes, tidyInterProAnnotations, fillPalette } =
|
|
47
|
+
model
|
|
48
|
+
const counts = new Map<string, number>()
|
|
49
|
+
for (const annot of tidyInterProAnnotations) {
|
|
50
|
+
counts.set(annot.accession, (counts.get(annot.accession) ?? 0) + 1)
|
|
51
|
+
}
|
|
42
52
|
return (
|
|
43
53
|
<>
|
|
44
54
|
<Toggles model={model} />
|
|
45
|
-
<
|
|
46
|
-
<
|
|
47
|
-
<
|
|
48
|
-
<
|
|
49
|
-
<
|
|
50
|
-
<
|
|
51
|
-
<
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
model.
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
55
|
+
<Table size="small">
|
|
56
|
+
<TableHead>
|
|
57
|
+
<TableRow>
|
|
58
|
+
<TableCell padding="checkbox" />
|
|
59
|
+
<TableCell>color</TableCell>
|
|
60
|
+
<TableCell>accession</TableCell>
|
|
61
|
+
<TableCell>name</TableCell>
|
|
62
|
+
<TableCell>count</TableCell>
|
|
63
|
+
<TableCell>description</TableCell>
|
|
64
|
+
</TableRow>
|
|
65
|
+
</TableHead>
|
|
66
|
+
<TableBody>
|
|
67
|
+
{[...tidyInterProAnnotationTypes.values()].map(
|
|
68
|
+
({ accession, name, description }) => (
|
|
69
|
+
<TableRow key={accession}>
|
|
70
|
+
<TableCell padding="checkbox">
|
|
71
|
+
<Checkbox
|
|
72
|
+
checked={model.featureFilters.get(accession) ?? false}
|
|
73
|
+
onChange={() => {
|
|
74
|
+
model.setFilter(
|
|
75
|
+
accession,
|
|
76
|
+
!model.featureFilters.get(accession),
|
|
77
|
+
)
|
|
78
|
+
}}
|
|
79
|
+
/>
|
|
80
|
+
</TableCell>
|
|
81
|
+
<TableCell>
|
|
82
|
+
<div
|
|
83
|
+
style={{
|
|
84
|
+
width: 20,
|
|
85
|
+
height: 20,
|
|
86
|
+
background: fillPalette[accession],
|
|
87
|
+
}}
|
|
88
|
+
/>
|
|
89
|
+
</TableCell>
|
|
90
|
+
<TableCell>{accession}</TableCell>
|
|
91
|
+
<TableCell>{name}</TableCell>
|
|
92
|
+
<TableCell>{counts.get(accession)}</TableCell>
|
|
93
|
+
<TableCell>{description}</TableCell>
|
|
94
|
+
</TableRow>
|
|
95
|
+
),
|
|
96
|
+
)}
|
|
97
|
+
</TableBody>
|
|
98
|
+
</Table>
|
|
85
99
|
</>
|
|
86
100
|
)
|
|
87
101
|
})
|
|
@@ -103,7 +117,7 @@ const FeatureTypeDialog = observer(function ({
|
|
|
103
117
|
maxWidth="xl"
|
|
104
118
|
>
|
|
105
119
|
<DialogContent>
|
|
106
|
-
<
|
|
120
|
+
<FeatureTable model={model} />
|
|
107
121
|
</DialogContent>
|
|
108
122
|
</Dialog>
|
|
109
123
|
)
|
|
Binary file
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import React, { useState } from 'react'
|
|
2
|
+
|
|
3
|
+
import ExpandLess from '@mui/icons-material/ExpandLess'
|
|
4
|
+
import ExpandMore from '@mui/icons-material/ExpandMore'
|
|
5
|
+
import { IconButton, Paper, Typography } from '@mui/material'
|
|
6
|
+
import { observer } from 'mobx-react'
|
|
7
|
+
|
|
8
|
+
import type { MsaViewModel } from '../../model.ts'
|
|
9
|
+
|
|
10
|
+
const DomainLegend = observer(function ({ model }: { model: MsaViewModel }) {
|
|
11
|
+
const [expanded, setExpanded] = useState(true)
|
|
12
|
+
const { actuallyShowDomains, visibleDomainTypes: visible, fillPalette } = model
|
|
13
|
+
const ExpandIcon = expanded ? ExpandLess : ExpandMore
|
|
14
|
+
|
|
15
|
+
return actuallyShowDomains && visible.length > 0 ? (
|
|
16
|
+
<Paper
|
|
17
|
+
elevation={3}
|
|
18
|
+
style={{
|
|
19
|
+
position: 'absolute',
|
|
20
|
+
top: 4,
|
|
21
|
+
right: 4,
|
|
22
|
+
zIndex: 100,
|
|
23
|
+
maxWidth: 260,
|
|
24
|
+
maxHeight: '60%',
|
|
25
|
+
display: 'flex',
|
|
26
|
+
flexDirection: 'column',
|
|
27
|
+
opacity: 0.95,
|
|
28
|
+
}}
|
|
29
|
+
>
|
|
30
|
+
<div
|
|
31
|
+
style={{
|
|
32
|
+
display: 'flex',
|
|
33
|
+
alignItems: 'center',
|
|
34
|
+
justifyContent: 'flex-end',
|
|
35
|
+
padding: '2px 4px 2px 8px',
|
|
36
|
+
}}
|
|
37
|
+
>
|
|
38
|
+
<IconButton
|
|
39
|
+
size="small"
|
|
40
|
+
onClick={() => {
|
|
41
|
+
setExpanded(!expanded)
|
|
42
|
+
}}
|
|
43
|
+
>
|
|
44
|
+
<ExpandIcon fontSize="small" />
|
|
45
|
+
</IconButton>
|
|
46
|
+
</div>
|
|
47
|
+
{expanded ? (
|
|
48
|
+
<div style={{ overflow: 'auto', padding: '0 8px 6px' }}>
|
|
49
|
+
{visible.map(({ accession, name }) => (
|
|
50
|
+
<div
|
|
51
|
+
key={accession}
|
|
52
|
+
style={{ display: 'flex', alignItems: 'center', gap: 6 }}
|
|
53
|
+
title={accession}
|
|
54
|
+
>
|
|
55
|
+
<div
|
|
56
|
+
style={{
|
|
57
|
+
width: 12,
|
|
58
|
+
height: 12,
|
|
59
|
+
flexShrink: 0,
|
|
60
|
+
background: fillPalette[accession],
|
|
61
|
+
}}
|
|
62
|
+
/>
|
|
63
|
+
<Typography variant="caption" noWrap>
|
|
64
|
+
{name}
|
|
65
|
+
</Typography>
|
|
66
|
+
</div>
|
|
67
|
+
))}
|
|
68
|
+
</div>
|
|
69
|
+
) : null}
|
|
70
|
+
</Paper>
|
|
71
|
+
) : null
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
export default DomainLegend
|
|
@@ -89,7 +89,7 @@ const MSACanvasBlock = observer(function ({
|
|
|
89
89
|
])
|
|
90
90
|
|
|
91
91
|
const [mousePosition, setMousePosition] = useState<{ x: number; y: number }>()
|
|
92
|
-
const { hoveredInsertion } = model
|
|
92
|
+
const { hoveredInsertion, mouseOverDomains } = model
|
|
93
93
|
|
|
94
94
|
return (
|
|
95
95
|
<>
|
|
@@ -116,10 +116,11 @@ const MSACanvasBlock = observer(function ({
|
|
|
116
116
|
} else {
|
|
117
117
|
model.setMousePos(undefined, undefined)
|
|
118
118
|
}
|
|
119
|
-
// only track client coords when
|
|
120
|
-
//
|
|
119
|
+
// only track client coords when there's something to show in the
|
|
120
|
+
// tooltip (insertion or a domain under the cursor); avoids a
|
|
121
|
+
// re-render on every move over plain alignment cells
|
|
121
122
|
setMousePosition(
|
|
122
|
-
model.hoveredInsertion
|
|
123
|
+
model.hoveredInsertion || model.mouseOverDomains.length > 0
|
|
123
124
|
? { x: event.clientX, y: event.clientY }
|
|
124
125
|
: undefined,
|
|
125
126
|
)
|
|
@@ -167,6 +168,23 @@ const MSACanvasBlock = observer(function ({
|
|
|
167
168
|
: hoveredInsertion.letters}
|
|
168
169
|
</BaseTooltip>
|
|
169
170
|
) : null}
|
|
171
|
+
{!hoveredInsertion && mouseOverDomains.length > 0 && mousePosition ? (
|
|
172
|
+
<BaseTooltip
|
|
173
|
+
clientPoint={{ x: mousePosition.x, y: mousePosition.y + 15 }}
|
|
174
|
+
>
|
|
175
|
+
{mouseOverDomains.map(d => (
|
|
176
|
+
<div key={`${d.accession}-${d.start}-${d.end}`}>
|
|
177
|
+
<b>{d.name}</b> ({d.accession}) {d.start}-{d.end}
|
|
178
|
+
{d.description ? (
|
|
179
|
+
<>
|
|
180
|
+
<br />
|
|
181
|
+
{d.description}
|
|
182
|
+
</>
|
|
183
|
+
) : null}
|
|
184
|
+
</div>
|
|
185
|
+
))}
|
|
186
|
+
</BaseTooltip>
|
|
187
|
+
) : null}
|
|
170
188
|
</>
|
|
171
189
|
)
|
|
172
190
|
})
|
|
@@ -2,6 +2,7 @@ import React from 'react'
|
|
|
2
2
|
|
|
3
3
|
import { observer } from 'mobx-react'
|
|
4
4
|
|
|
5
|
+
import DomainLegend from './DomainLegend.tsx'
|
|
5
6
|
import MSACanvas from './MSACanvas.tsx'
|
|
6
7
|
import MSAMouseoverCanvas from './MSAMouseoverCanvas.tsx'
|
|
7
8
|
|
|
@@ -13,6 +14,7 @@ const MSAPanel = observer(function ({ model }: { model: MsaViewModel }) {
|
|
|
13
14
|
<div style={{ position: 'relative' }}>
|
|
14
15
|
<MSACanvas model={model} />
|
|
15
16
|
<MSAMouseoverCanvas model={model} />
|
|
17
|
+
<DomainLegend model={model} />
|
|
16
18
|
</div>
|
|
17
19
|
)
|
|
18
20
|
})
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getVisibleLeaves } from './getVisibleLeaves.ts'
|
|
2
|
+
import { subFeatureRowHeight } from '../../constants.ts'
|
|
2
3
|
|
|
3
4
|
import type { HierarchyNode } from '../../hierarchy.ts'
|
|
4
5
|
import type { MsaViewModel } from '../../model.ts'
|
|
@@ -53,38 +54,93 @@ function drawTiles({
|
|
|
53
54
|
rowHeight,
|
|
54
55
|
fillPalette,
|
|
55
56
|
strokePalette,
|
|
57
|
+
segmentLabels,
|
|
58
|
+
showMsaLetters,
|
|
56
59
|
tidyFilteredGatheredInterProAnnotations,
|
|
57
60
|
} = model
|
|
61
|
+
const h = subFeatureRows ? subFeatureRowHeight : rowHeight
|
|
62
|
+
// exon numbers label the bands only when residue letters aren't drawn (zoomed
|
|
63
|
+
// out); when letters show, the alternating shades alone mark the boundaries
|
|
64
|
+
// and a number would collide with the sequence
|
|
65
|
+
const drawSegmentLabels = !showMsaLetters && !subFeatureRows && h >= 9
|
|
58
66
|
|
|
59
67
|
for (let i = 0, l1 = visibleLeaves.length; i < l1; i++) {
|
|
60
68
|
const node = visibleLeaves[i]!
|
|
61
|
-
const {
|
|
62
|
-
|
|
63
|
-
data: { name },
|
|
64
|
-
} = node
|
|
65
|
-
const y = x!
|
|
66
|
-
|
|
69
|
+
const { name } = node.data
|
|
70
|
+
const y = node.x!
|
|
67
71
|
const entry = tidyFilteredGatheredInterProAnnotations[name]
|
|
68
72
|
|
|
69
73
|
if (entry) {
|
|
70
74
|
for (let j = 0, l2 = entry.length; j < l2; j++) {
|
|
71
|
-
const { start, end, accession } = entry[j]!
|
|
75
|
+
const { start, end, accession, strand } = entry[j]!
|
|
72
76
|
// Convert sequence positions to visible column positions
|
|
73
77
|
// seqPos is 1-based from InterPro, so subtract 1 for 0-based
|
|
74
78
|
const m1 = model.seqPosToVisibleCol(name, start - 1)
|
|
75
79
|
const m2 = model.seqPosToVisibleCol(name, end)
|
|
76
|
-
if (m1
|
|
77
|
-
|
|
80
|
+
if (m1 !== undefined && m2 !== undefined) {
|
|
81
|
+
const x = m1 * colWidth
|
|
82
|
+
const t = y - rowHeight + (subFeatureRows ? j * h : 0)
|
|
83
|
+
const lw = colWidth * (m2 - m1)
|
|
84
|
+
ctx.fillStyle = fillPalette[accession]!
|
|
85
|
+
ctx.strokeStyle = strokePalette[accession]!
|
|
86
|
+
if (strand === undefined) {
|
|
87
|
+
ctx.fillRect(x, t, lw, h)
|
|
88
|
+
ctx.strokeRect(x, t, lw, h)
|
|
89
|
+
// segment (exon) number drawn once per band, on the top visible row,
|
|
90
|
+
// so it reads as a column header for the whole band
|
|
91
|
+
const label = drawSegmentLabels ? segmentLabels.get(accession) : undefined
|
|
92
|
+
if (label !== undefined && i === 0) {
|
|
93
|
+
const fontSize = Math.min(h - 2, 11)
|
|
94
|
+
ctx.font = `${fontSize}px sans-serif`
|
|
95
|
+
if (ctx.measureText(label).width + 2 <= lw) {
|
|
96
|
+
ctx.fillStyle = '#222'
|
|
97
|
+
ctx.textAlign = 'center'
|
|
98
|
+
ctx.textBaseline = 'middle'
|
|
99
|
+
ctx.fillText(label, x + lw / 2, t + h / 2)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
} else {
|
|
103
|
+
drawGeneArrow({ ctx, x, t, w: lw, h, strand })
|
|
104
|
+
}
|
|
78
105
|
}
|
|
79
|
-
const x = m1 * colWidth
|
|
80
|
-
ctx.fillStyle = fillPalette[accession]!
|
|
81
|
-
ctx.strokeStyle = strokePalette[accession]!
|
|
82
|
-
const h = subFeatureRows ? 4 : rowHeight
|
|
83
|
-
const t = y - rowHeight + (subFeatureRows ? j * h : 0)
|
|
84
|
-
const lw = colWidth * (m2 - m1)
|
|
85
|
-
ctx.fillRect(x, t, lw, h)
|
|
86
|
-
ctx.strokeRect(x, t, lw, h)
|
|
87
106
|
}
|
|
88
107
|
}
|
|
89
108
|
}
|
|
90
109
|
}
|
|
110
|
+
|
|
111
|
+
// A gene arrow: a full-width rectangular body spanning the feature's
|
|
112
|
+
// start..end columns, plus a triangular head that points *beyond* that end in
|
|
113
|
+
// the strand direction (right for +, left for -). Keeping the body aligned to
|
|
114
|
+
// the exact start/end columns means + and - strand features read as having the
|
|
115
|
+
// same boundaries; the arrow lives outside them purely to show transcription
|
|
116
|
+
// direction, preserving the column-by-column homology down the rows.
|
|
117
|
+
function drawGeneArrow({
|
|
118
|
+
ctx,
|
|
119
|
+
x,
|
|
120
|
+
t,
|
|
121
|
+
w,
|
|
122
|
+
h,
|
|
123
|
+
strand,
|
|
124
|
+
}: {
|
|
125
|
+
ctx: RenderCtx
|
|
126
|
+
x: number
|
|
127
|
+
t: number
|
|
128
|
+
w: number
|
|
129
|
+
h: number
|
|
130
|
+
strand: number
|
|
131
|
+
}) {
|
|
132
|
+
// body spans bodyStart..bodyEnd (the exact columns); the head extends one
|
|
133
|
+
// row-height past bodyEnd in the strand direction
|
|
134
|
+
const dir = strand > 0 ? 1 : -1
|
|
135
|
+
const bodyStart = strand > 0 ? x : x + w
|
|
136
|
+
const bodyEnd = strand > 0 ? x + w : x
|
|
137
|
+
ctx.beginPath()
|
|
138
|
+
ctx.moveTo(bodyStart, t)
|
|
139
|
+
ctx.lineTo(bodyEnd, t)
|
|
140
|
+
ctx.lineTo(bodyEnd + dir * h, t + h / 2)
|
|
141
|
+
ctx.lineTo(bodyEnd, t + h)
|
|
142
|
+
ctx.lineTo(bodyStart, t + h)
|
|
143
|
+
ctx.closePath()
|
|
144
|
+
ctx.fill()
|
|
145
|
+
ctx.stroke()
|
|
146
|
+
}
|
|
@@ -46,6 +46,7 @@ export function renderMSABlock({
|
|
|
46
46
|
ctx.scale(k, k)
|
|
47
47
|
ctx.translate(-offsetX, rowHeight / 2 - offsetY)
|
|
48
48
|
ctx.textAlign = 'center'
|
|
49
|
+
ctx.textBaseline = 'alphabetic'
|
|
49
50
|
setFontSize(ctx, fontSize, !bgColor)
|
|
50
51
|
|
|
51
52
|
const { xStart, xEnd } = visibleColRange({
|
|
@@ -124,8 +125,7 @@ function drawTilesAndText({
|
|
|
124
125
|
|
|
125
126
|
const isClustalX = colorSchemeName === 'clustalx_protein_dynamic'
|
|
126
127
|
const isPercentIdentity = colorSchemeName === 'percent_identity_dynamic'
|
|
127
|
-
|
|
128
|
-
if (!drawBgTiles && !showMsaLetters) {
|
|
128
|
+
if (!bgColor && !showMsaLetters) {
|
|
129
129
|
return
|
|
130
130
|
}
|
|
131
131
|
const offsetXAligned = xStart * colWidth
|
|
@@ -159,7 +159,7 @@ function drawTilesAndText({
|
|
|
159
159
|
color = colorScheme[letter]
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
-
if (
|
|
162
|
+
if (bgColor) {
|
|
163
163
|
ctx.fillStyle = isMatchingReference
|
|
164
164
|
? theme.palette.action.hover
|
|
165
165
|
: color || theme.palette.background.default
|
|
@@ -2,9 +2,31 @@ import type { MsaViewModel } from '../../model.ts'
|
|
|
2
2
|
|
|
3
3
|
const hoverColor = 'rgba(0,0,0,0.15)'
|
|
4
4
|
const highlightColor = 'rgba(128,128,0,0.2)'
|
|
5
|
+
// the persistent highlightColumns overlay: a stronger fill plus a solid border
|
|
6
|
+
// so a domain/motif band reads clearly over the colored alignment cells (the
|
|
7
|
+
// faint hover-style wash alone is invisible against clustalx coloring)
|
|
8
|
+
const highlightColumnsFill = 'rgba(255,140,0,0.28)'
|
|
9
|
+
const highlightColumnsBorder = 'rgba(210,90,0,0.95)'
|
|
5
10
|
const referenceColor = 'rgba(0,128,255,0.3)' // Blue highlight for reference row
|
|
6
11
|
const multiRowHoverColor = 'rgba(255,165,0,0.15)' // Orange highlight for multi-row tree hover
|
|
7
12
|
|
|
13
|
+
// Collapse sorted column indices into contiguous [start,end] runs so a run of
|
|
14
|
+
// highlighted columns draws as one bordered band rather than per-column slices
|
|
15
|
+
// (which would draw internal borders).
|
|
16
|
+
function contiguousRuns(columns: number[]) {
|
|
17
|
+
const sorted = [...columns].sort((a, b) => a - b)
|
|
18
|
+
const runs: { start: number; end: number }[] = []
|
|
19
|
+
for (const col of sorted) {
|
|
20
|
+
const last = runs.at(-1)
|
|
21
|
+
if (last && col === last.end + 1) {
|
|
22
|
+
last.end = col
|
|
23
|
+
} else {
|
|
24
|
+
runs.push({ start: col, end: col })
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return runs
|
|
28
|
+
}
|
|
29
|
+
|
|
8
30
|
export function renderMouseover({
|
|
9
31
|
ctx,
|
|
10
32
|
model,
|
|
@@ -54,11 +76,17 @@ export function renderMouseover({
|
|
|
54
76
|
}
|
|
55
77
|
}
|
|
56
78
|
|
|
57
|
-
// Highlight multiple columns
|
|
79
|
+
// Highlight multiple columns — draw each contiguous run as a filled, bordered
|
|
80
|
+
// band so a domain/motif highlight is clearly visible over the alignment
|
|
58
81
|
if (highlightedColumns?.length) {
|
|
59
|
-
ctx.
|
|
60
|
-
for (const
|
|
61
|
-
|
|
82
|
+
ctx.lineWidth = 2
|
|
83
|
+
for (const { start, end } of contiguousRuns(highlightedColumns)) {
|
|
84
|
+
const x = start * colWidth + scrollX
|
|
85
|
+
const w = (end - start + 1) * colWidth
|
|
86
|
+
ctx.fillStyle = highlightColumnsFill
|
|
87
|
+
ctx.fillRect(x, 0, w, height)
|
|
88
|
+
ctx.strokeStyle = highlightColumnsBorder
|
|
89
|
+
ctx.strokeRect(x, 0, w, height)
|
|
62
90
|
}
|
|
63
91
|
}
|
|
64
92
|
|
|
@@ -65,13 +65,13 @@ export function drawTextTrackContent({
|
|
|
65
65
|
|
|
66
66
|
for (let i = 0; str && i < str.length; i++) {
|
|
67
67
|
const letter = str[i]!
|
|
68
|
-
const
|
|
68
|
+
const upper = letter.toUpperCase()
|
|
69
69
|
if (bgColor) {
|
|
70
70
|
const x = (xStart + i) * colWidth
|
|
71
|
-
ctx.fillStyle =
|
|
71
|
+
ctx.fillStyle = colorScheme[upper] || 'white'
|
|
72
72
|
ctx.fillRect(x, 0, colWidth, rowHeight)
|
|
73
73
|
if (rowHeight >= 10 && colWidth >= rowHeight / 2) {
|
|
74
|
-
ctx.fillStyle = contrastScheme[
|
|
74
|
+
ctx.fillStyle = contrastScheme[upper] || 'black'
|
|
75
75
|
ctx.fillText(letter, x + colWidth / 2, rowHeight / 2 + 1)
|
|
76
76
|
}
|
|
77
77
|
}
|
package/src/constants.ts
CHANGED
|
@@ -9,6 +9,24 @@ export const defaultShowDomains = false
|
|
|
9
9
|
export const defaultHideGaps = true
|
|
10
10
|
export const defaultAllowedGappyness = 100
|
|
11
11
|
export const defaultSubFeatureRows = false
|
|
12
|
+
// row height for each stacked feature when subFeatureRows is on
|
|
13
|
+
export const subFeatureRowHeight = 4
|
|
14
|
+
|
|
15
|
+
// Feature types that are ordinal *segments* of a single transcript (exons and
|
|
16
|
+
// the like) rather than categorical domains. Their identity is their position,
|
|
17
|
+
// so the overlay alternates two shades to mark boundaries and labels them by
|
|
18
|
+
// number, instead of assigning every one a distinct hue + legend row.
|
|
19
|
+
export const segmentFeatureTypes = new Set([
|
|
20
|
+
'exon',
|
|
21
|
+
'CDS',
|
|
22
|
+
'five_prime_UTR',
|
|
23
|
+
'three_prime_UTR',
|
|
24
|
+
'UTR',
|
|
25
|
+
'intron',
|
|
26
|
+
])
|
|
27
|
+
|
|
28
|
+
// the two shades alternated across adjacent segments (exons)
|
|
29
|
+
export const segmentShades = ['#9fb6d4', '#d4dcea']
|
|
12
30
|
export const defaultDrawMsaLetters = true
|
|
13
31
|
export const defaultScrollZoom = false
|
|
14
32
|
|