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.
Files changed (77) hide show
  1. package/LICENSE +21 -0
  2. package/bundle/index.js +465 -138
  3. package/bundle/index.js.map +7 -1
  4. package/dist/colorSchemes.js +11 -7
  5. package/dist/colorSchemes.js.map +1 -1
  6. package/dist/components/MSAViewer.d.ts +7 -1
  7. package/dist/components/MSAViewer.js +10 -7
  8. package/dist/components/MSAViewer.js.map +1 -1
  9. package/dist/components/Track.js +8 -5
  10. package/dist/components/Track.js.map +1 -1
  11. package/dist/components/dialogs/FeatureDialog.js +27 -23
  12. package/dist/components/dialogs/FeatureDialog.js.map +1 -1
  13. package/dist/components/header/MultiAlignmentSelector.js +0 -0
  14. package/dist/components/header/MultiAlignmentSelector.js.map +1 -1
  15. package/dist/components/msa/DomainLegend.d.ts +6 -0
  16. package/dist/components/msa/DomainLegend.js +41 -0
  17. package/dist/components/msa/DomainLegend.js.map +1 -0
  18. package/dist/components/msa/MSACanvasBlock.js +17 -5
  19. package/dist/components/msa/MSACanvasBlock.js.map +1 -1
  20. package/dist/components/msa/MSAPanel.js +3 -1
  21. package/dist/components/msa/MSAPanel.js.map +1 -1
  22. package/dist/components/msa/renderBoxFeatureCanvasBlock.js +58 -14
  23. package/dist/components/msa/renderBoxFeatureCanvasBlock.js.map +1 -1
  24. package/dist/components/msa/renderMSABlock.js +3 -3
  25. package/dist/components/msa/renderMSABlock.js.map +1 -1
  26. package/dist/components/msa/renderMSAMouseover.js +32 -4
  27. package/dist/components/msa/renderMSAMouseover.js.map +1 -1
  28. package/dist/components/renderCtx.d.ts +1 -1
  29. package/dist/components/tracks/renderTracksSvg.js +3 -3
  30. package/dist/components/tracks/renderTracksSvg.js.map +1 -1
  31. package/dist/constants.d.ts +3 -0
  32. package/dist/constants.js +16 -0
  33. package/dist/constants.js.map +1 -1
  34. package/dist/createPaletteMap.d.ts +5 -2
  35. package/dist/createPaletteMap.js +15 -5
  36. package/dist/createPaletteMap.js.map +1 -1
  37. package/dist/launchInterProScan.d.ts +2 -0
  38. package/dist/launchInterProScan.js.map +1 -1
  39. package/dist/model.d.ts +96 -1
  40. package/dist/model.js +91 -3
  41. package/dist/model.js.map +1 -1
  42. package/dist/renderToSvg.js +104 -74
  43. package/dist/renderToSvg.js.map +1 -1
  44. package/dist/{webpack.js → umd.js} +1 -1
  45. package/dist/umd.js.map +1 -0
  46. package/dist/version.d.ts +1 -1
  47. package/dist/version.js +1 -1
  48. package/package.json +7 -6
  49. package/src/colorSchemes.ts +12 -7
  50. package/src/components/MSAViewer.tsx +30 -18
  51. package/src/components/Track.tsx +8 -5
  52. package/src/components/dialogs/FeatureDialog.tsx +62 -48
  53. package/src/components/header/MultiAlignmentSelector.tsx +0 -0
  54. package/src/components/msa/DomainLegend.tsx +74 -0
  55. package/src/components/msa/MSACanvasBlock.tsx +22 -4
  56. package/src/components/msa/MSAPanel.tsx +2 -0
  57. package/src/components/msa/renderBoxFeatureCanvasBlock.ts +73 -17
  58. package/src/components/msa/renderMSABlock.ts +3 -3
  59. package/src/components/msa/renderMSAMouseover.ts +32 -4
  60. package/src/components/renderCtx.ts +1 -0
  61. package/src/components/tracks/renderTracksSvg.ts +3 -3
  62. package/src/constants.ts +18 -0
  63. package/src/createPaletteMap.ts +18 -5
  64. package/src/launchInterProScan.ts +3 -1
  65. package/src/model.ts +105 -2
  66. package/src/renderDomainLegendSvg.test.tsx +134 -0
  67. package/src/renderToSvg.tsx +210 -147
  68. package/src/segmentOverlay.test.ts +70 -0
  69. package/src/version.ts +1 -1
  70. package/bundle/index.js.LICENSE.txt +0 -100
  71. package/dist/components/util.d.ts +0 -1
  72. package/dist/components/util.js +0 -7
  73. package/dist/components/util.js.map +0 -1
  74. package/dist/webpack.js.map +0 -1
  75. package/src/components/util.ts +0 -9
  76. /package/dist/{webpack.d.ts → umd.d.ts} +0 -0
  77. /package/src/{webpack.ts → umd.ts} +0 -0
@@ -1,4 +1,4 @@
1
- import React, { useEffect, useMemo } from 'react'
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
- const theme = useMemo(() => createJBrowseTheme(), [])
37
- const model = useMemo(
38
- () =>
39
- MSAModelF().create({
40
- type: 'MsaView',
41
- ...(msa || tree || gff
42
- ? { data: { msa: msa ?? '', tree: tree ?? '', gff } }
43
- : {}),
44
- ...(msaFilehandle ? { msaFilehandle } : {}),
45
- ...(treeFilehandle ? { treeFilehandle } : {}),
46
- ...(gffFilehandle ? { gffFilehandle } : {}),
47
- ...(colorScheme ? { colorSchemeName: colorScheme } : {}),
48
- ...(height ? { height } : {}),
49
- ...(relativeTo ? { relativeTo } : {}),
50
- }),
51
- // eslint-disable-next-line react-hooks/exhaustive-deps
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()
@@ -26,12 +26,15 @@ const TrackLabel = observer(function TrackLabel({
26
26
  track: BasicTrack
27
27
  }) {
28
28
  const [anchorEl, setAnchorEl] = useState<HTMLButtonElement>()
29
- const { drawLabels, rowHeight, treeAreaWidth: width } = model
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, Math.max(8, rowHeight - 8))
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
- setAnchorEl(undefined)
68
+ closeMenu()
66
69
  }}
67
70
  >
68
71
  <MenuItem
69
72
  dense
70
73
  onClick={() => {
71
74
  model.toggleTrack(track.model.id)
72
- setAnchorEl(undefined)
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
- setAnchorEl(undefined)
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 { Button, DialogContent } from '@mui/material'
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 Table = observer(function ({ model }: { model: MsaViewModel }) {
39
- const { tidyInterProAnnotationTypes, featureFilters } = model
40
- const values = [...tidyInterProAnnotationTypes.values()]
41
- const palette = getPalette(values.length - 1)
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
- <table>
46
- <thead>
47
- <tr>
48
- <td />
49
- <td>accession</td>
50
- <td>name</td>
51
- <td>description</td>
52
- </tr>
53
- </thead>
54
- <tbody>
55
- {values.map(({ accession, name, description }, idx) => (
56
- <tr key={accession}>
57
- <td>
58
- <input
59
- type="checkbox"
60
- checked={featureFilters.get(accession) ?? false}
61
- onChange={() => {
62
- model.setFilter(
63
- accession,
64
- !model.featureFilters.get(accession),
65
- )
66
- }}
67
- />
68
- </td>
69
- <td>{accession}</td>
70
- <td>{name}</td>
71
- <td>{description}</td>
72
- <td>
73
- <div
74
- style={{
75
- width: 20,
76
- height: 20,
77
- background: palette[idx] || 'black',
78
- }}
79
- />
80
- </td>
81
- </tr>
82
- ))}
83
- </tbody>
84
- </table>
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
- <Table model={model} />
120
+ <FeatureTable model={model} />
107
121
  </DialogContent>
108
122
  </Dialog>
109
123
  )
@@ -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 an insertion is hovered (the only
120
- // consumer is the tooltip below); avoids a re-render on every move
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
- x,
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 === undefined || m2 === undefined) {
77
- continue // Skip if either position is hidden
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
- const drawBgTiles = bgColor || isClustalX || isPercentIdentity
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 (drawBgTiles) {
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.fillStyle = highlightColor
60
- for (const col of highlightedColumns) {
61
- ctx.fillRect(col * colWidth + scrollX, 0, colWidth, height)
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
 
@@ -26,5 +26,6 @@ export type RenderCtx = Pick<
26
26
  | 'strokeRect'
27
27
  | 'strokeStyle'
28
28
  | 'textAlign'
29
+ | 'textBaseline'
29
30
  | 'translate'
30
31
  >
@@ -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 color = colorScheme[letter.toUpperCase()]
68
+ const upper = letter.toUpperCase()
69
69
  if (bgColor) {
70
70
  const x = (xStart + i) * colWidth
71
- ctx.fillStyle = color || 'white'
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[letter.toUpperCase()] || 'black'
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