react-msaview 5.4.1 → 5.6.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 (180) hide show
  1. package/LICENSE +21 -0
  2. package/bundle/index.js +487 -149
  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/ConservationTrack.js +20 -26
  7. package/dist/components/ConservationTrack.js.map +1 -1
  8. package/dist/components/MSAViewer.d.ts +13 -1
  9. package/dist/components/MSAViewer.js +13 -7
  10. package/dist/components/MSAViewer.js.map +1 -1
  11. package/dist/components/SequenceTextArea.js +1 -1
  12. package/dist/components/SequenceTextArea.js.map +1 -1
  13. package/dist/components/TextTrack.js +20 -27
  14. package/dist/components/TextTrack.js.map +1 -1
  15. package/dist/components/Track.js +9 -6
  16. package/dist/components/Track.js.map +1 -1
  17. package/dist/components/dialogs/FeatureDialog.js +27 -23
  18. package/dist/components/dialogs/FeatureDialog.js.map +1 -1
  19. package/dist/components/dialogs/TrackInfoDialog.js +1 -1
  20. package/dist/components/dialogs/TrackInfoDialog.js.map +1 -1
  21. package/dist/components/header/ColorSchemeMenu.js +1 -1
  22. package/dist/components/header/ColorSchemeMenu.js.map +1 -1
  23. package/dist/components/header/FileMenu.js +1 -16
  24. package/dist/components/header/FileMenu.js.map +1 -1
  25. package/dist/components/header/Header.js +6 -5
  26. package/dist/components/header/Header.js.map +1 -1
  27. package/dist/components/header/HeaderInfoArea.js +1 -1
  28. package/dist/components/header/HeaderInfoArea.js.map +1 -1
  29. package/dist/components/header/HeaderStatusArea.js +1 -1
  30. package/dist/components/header/HeaderStatusArea.js.map +1 -1
  31. package/dist/components/header/MSASettingsMenu.js +1 -1
  32. package/dist/components/header/MSASettingsMenu.js.map +1 -1
  33. package/dist/components/header/MultiAlignmentSelector.js +0 -0
  34. package/dist/components/header/MultiAlignmentSelector.js.map +1 -1
  35. package/dist/components/header/TreeSettingsMenu.js +1 -1
  36. package/dist/components/header/TreeSettingsMenu.js.map +1 -1
  37. package/dist/components/header/ZoomControls.js +14 -12
  38. package/dist/components/header/ZoomControls.js.map +1 -1
  39. package/dist/components/header/ZoomMenu.js +1 -10
  40. package/dist/components/header/ZoomMenu.js.map +1 -1
  41. package/dist/components/header/settingsMenuItems.js +9 -1
  42. package/dist/components/header/settingsMenuItems.js.map +1 -1
  43. package/dist/components/{header/HeaderMenu.d.ts → msa/ColumnStats.d.ts} +3 -3
  44. package/dist/components/msa/ColumnStats.js +42 -0
  45. package/dist/components/msa/ColumnStats.js.map +1 -0
  46. package/dist/components/msa/DomainLegend.d.ts +6 -0
  47. package/dist/components/msa/DomainLegend.js +41 -0
  48. package/dist/components/msa/DomainLegend.js.map +1 -0
  49. package/dist/components/msa/MSACanvasBlock.js +41 -29
  50. package/dist/components/msa/MSACanvasBlock.js.map +1 -1
  51. package/dist/components/msa/MSAMouseoverCanvas.js +6 -12
  52. package/dist/components/msa/MSAMouseoverCanvas.js.map +1 -1
  53. package/dist/components/msa/MSAPanel.js +3 -1
  54. package/dist/components/msa/MSAPanel.js.map +1 -1
  55. package/dist/components/msa/renderBoxFeatureCanvasBlock.js +60 -14
  56. package/dist/components/msa/renderBoxFeatureCanvasBlock.js.map +1 -1
  57. package/dist/components/msa/renderMSABlock.js +3 -3
  58. package/dist/components/msa/renderMSABlock.js.map +1 -1
  59. package/dist/components/msa/renderMSAMouseover.js +33 -6
  60. package/dist/components/msa/renderMSAMouseover.js.map +1 -1
  61. package/dist/components/overlayColors.d.ts +3 -0
  62. package/dist/components/overlayColors.js +11 -0
  63. package/dist/components/overlayColors.js.map +1 -0
  64. package/dist/components/renderCtx.d.ts +1 -1
  65. package/dist/components/tracks/renderTracksSvg.d.ts +6 -3
  66. package/dist/components/tracks/renderTracksSvg.js +20 -11
  67. package/dist/components/tracks/renderTracksSvg.js.map +1 -1
  68. package/dist/components/tree/TreeCanvas.js +31 -38
  69. package/dist/components/tree/TreeCanvas.js.map +1 -1
  70. package/dist/components/tree/TreeCanvasBlock.js +12 -47
  71. package/dist/components/tree/TreeCanvasBlock.js.map +1 -1
  72. package/dist/components/tree/clickMap.d.ts +24 -0
  73. package/dist/components/tree/clickMap.js +33 -0
  74. package/dist/components/tree/clickMap.js.map +1 -0
  75. package/dist/components/tree/renderTreeCanvas.d.ts +1 -21
  76. package/dist/components/tree/renderTreeCanvas.js +8 -7
  77. package/dist/components/tree/renderTreeCanvas.js.map +1 -1
  78. package/dist/constants.d.ts +4 -0
  79. package/dist/constants.js +17 -0
  80. package/dist/constants.js.map +1 -1
  81. package/dist/createPaletteMap.d.ts +5 -2
  82. package/dist/createPaletteMap.js +15 -5
  83. package/dist/createPaletteMap.js.map +1 -1
  84. package/dist/fetchUtils.d.ts +2 -1
  85. package/dist/fetchUtils.js +3 -2
  86. package/dist/fetchUtils.js.map +1 -1
  87. package/dist/index.d.ts +1 -1
  88. package/dist/launchInterProScan.d.ts +2 -0
  89. package/dist/launchInterProScan.js.map +1 -1
  90. package/dist/model/msaModel.d.ts +25 -2
  91. package/dist/model/msaModel.js +15 -3
  92. package/dist/model/msaModel.js.map +1 -1
  93. package/dist/model/treeModel.d.ts +46 -5
  94. package/dist/model/treeModel.js +20 -7
  95. package/dist/model/treeModel.js.map +1 -1
  96. package/dist/model.d.ts +641 -133
  97. package/dist/model.js +191 -77
  98. package/dist/model.js.map +1 -1
  99. package/dist/propertyConservation.d.ts +7 -0
  100. package/dist/propertyConservation.js +66 -0
  101. package/dist/propertyConservation.js.map +1 -0
  102. package/dist/renderToSvg.js +104 -74
  103. package/dist/renderToSvg.js.map +1 -1
  104. package/dist/types.d.ts +4 -1
  105. package/dist/{webpack.js → umd.js} +1 -1
  106. package/dist/umd.js.map +1 -0
  107. package/dist/useCanvasAutorun.d.ts +2 -0
  108. package/dist/useCanvasAutorun.js +23 -0
  109. package/dist/useCanvasAutorun.js.map +1 -0
  110. package/dist/version.d.ts +1 -1
  111. package/dist/version.js +1 -1
  112. package/package.json +7 -8
  113. package/src/colorSchemes.docs.test.ts +55 -0
  114. package/src/colorSchemes.ts +12 -7
  115. package/src/columnStats.test.ts +94 -0
  116. package/src/components/ConservationTrack.tsx +17 -15
  117. package/src/components/MSAViewer.tsx +42 -18
  118. package/src/components/SequenceTextArea.tsx +1 -1
  119. package/src/components/TextTrack.tsx +7 -11
  120. package/src/components/Track.tsx +9 -6
  121. package/src/components/dialogs/FeatureDialog.tsx +62 -48
  122. package/src/components/dialogs/TrackInfoDialog.tsx +1 -1
  123. package/src/components/header/ColorSchemeMenu.tsx +1 -0
  124. package/src/components/header/FileMenu.tsx +1 -15
  125. package/src/components/header/Header.tsx +11 -8
  126. package/src/components/header/HeaderInfoArea.tsx +1 -1
  127. package/src/components/header/HeaderStatusArea.tsx +1 -1
  128. package/src/components/header/MSASettingsMenu.tsx +1 -0
  129. package/src/components/header/MultiAlignmentSelector.tsx +0 -0
  130. package/src/components/header/TreeSettingsMenu.tsx +1 -0
  131. package/src/components/header/ZoomControls.tsx +24 -17
  132. package/src/components/header/ZoomMenu.tsx +1 -10
  133. package/src/components/header/settingsMenuItems.ts +9 -1
  134. package/src/components/msa/ColumnStats.tsx +54 -0
  135. package/src/components/msa/DomainLegend.tsx +78 -0
  136. package/src/components/msa/MSACanvasBlock.tsx +46 -25
  137. package/src/components/msa/MSAMouseoverCanvas.tsx +10 -13
  138. package/src/components/msa/MSAPanel.tsx +2 -0
  139. package/src/components/msa/renderBoxFeatureCanvasBlock.ts +75 -17
  140. package/src/components/msa/renderMSABlock.ts +3 -3
  141. package/src/components/msa/renderMSAMouseover.ts +34 -6
  142. package/src/components/overlayColors.ts +12 -0
  143. package/src/components/renderCtx.ts +1 -0
  144. package/src/components/tracks/renderTracksSvg.ts +25 -11
  145. package/src/components/tree/TreeCanvas.tsx +47 -54
  146. package/src/components/tree/TreeCanvasBlock.tsx +10 -68
  147. package/src/components/tree/clickMap.ts +55 -0
  148. package/src/components/tree/renderTreeCanvas.ts +10 -34
  149. package/src/constants.ts +19 -0
  150. package/src/createPaletteMap.ts +18 -5
  151. package/src/fetchUtils.ts +6 -3
  152. package/src/index.ts +1 -1
  153. package/src/launchInterProScan.ts +3 -1
  154. package/src/model/msaModel.ts +21 -3
  155. package/src/model/treeModel.ts +21 -7
  156. package/src/model.ts +221 -119
  157. package/src/propertyConservation.test.ts +47 -0
  158. package/src/propertyConservation.ts +73 -0
  159. package/src/renderDomainLegendSvg.test.tsx +134 -0
  160. package/src/renderToSvg.tsx +210 -147
  161. package/src/segmentOverlay.test.ts +70 -0
  162. package/src/stripDefaultSnapshot.test.ts +54 -0
  163. package/src/types.ts +7 -1
  164. package/src/useCanvasAutorun.ts +28 -0
  165. package/src/version.ts +1 -1
  166. package/bundle/index.js.LICENSE.txt +0 -100
  167. package/dist/components/dialogs/SettingsDialog.d.ts +0 -7
  168. package/dist/components/dialogs/SettingsDialog.js +0 -100
  169. package/dist/components/dialogs/SettingsDialog.js.map +0 -1
  170. package/dist/components/header/HeaderMenu.js +0 -27
  171. package/dist/components/header/HeaderMenu.js.map +0 -1
  172. package/dist/components/util.d.ts +0 -1
  173. package/dist/components/util.js +0 -7
  174. package/dist/components/util.js.map +0 -1
  175. package/dist/webpack.js.map +0 -1
  176. package/src/components/dialogs/SettingsDialog.tsx +0 -234
  177. package/src/components/header/HeaderMenu.tsx +0 -38
  178. package/src/components/util.ts +0 -9
  179. /package/dist/{webpack.d.ts → umd.d.ts} +0 -0
  180. /package/src/{webpack.ts → umd.ts} +0 -0
@@ -1,9 +1,9 @@
1
1
  import React from 'react'
2
2
 
3
+ import Mouse from '@mui/icons-material/Mouse'
3
4
  import ZoomIn from '@mui/icons-material/ZoomIn'
4
- import ZoomInMap from '@mui/icons-material/ZoomInMap'
5
5
  import ZoomOut from '@mui/icons-material/ZoomOut'
6
- import { IconButton, ToggleButton } from '@mui/material'
6
+ import { IconButton, ToggleButton, Tooltip } from '@mui/material'
7
7
  import { observer } from 'mobx-react'
8
8
 
9
9
  import type { MsaViewModel } from '../../model.ts'
@@ -15,23 +15,30 @@ const ZoomControls = observer(function ZoomControls({
15
15
  }) {
16
16
  return (
17
17
  <>
18
- <IconButton
19
- onClick={() => {
20
- model.zoomIn()
21
- }}
22
- >
23
- <ZoomIn />
24
- </IconButton>
25
- <IconButton
26
- onClick={() => {
27
- model.zoomOut()
28
- }}
29
- >
30
- <ZoomOut />
31
- </IconButton>
18
+ <Tooltip title="Zoom in">
19
+ <IconButton
20
+ aria-label="Zoom in"
21
+ onClick={() => {
22
+ model.zoomIn()
23
+ }}
24
+ >
25
+ <ZoomIn />
26
+ </IconButton>
27
+ </Tooltip>
28
+ <Tooltip title="Zoom out">
29
+ <IconButton
30
+ aria-label="Zoom out"
31
+ onClick={() => {
32
+ model.zoomOut()
33
+ }}
34
+ >
35
+ <ZoomOut />
36
+ </IconButton>
37
+ </Tooltip>
32
38
  <ToggleButton
33
39
  value="scrollZoom"
34
40
  selected={model.scrollZoom}
41
+ aria-label="Toggle scroll-to-zoom"
35
42
  title="Toggle scroll-to-zoom (zoom on plain mouse wheel; ctrl+wheel always zooms)"
36
43
  size="small"
37
44
  sx={{ border: 'none' }}
@@ -39,7 +46,7 @@ const ZoomControls = observer(function ZoomControls({
39
46
  model.setScrollZoom(!model.scrollZoom)
40
47
  }}
41
48
  >
42
- <ZoomInMap />
49
+ <Mouse />
43
50
  </ToggleButton>
44
51
  </>
45
52
  )
@@ -10,6 +10,7 @@ import type { MsaViewModel } from '../../model.ts'
10
10
  const ZoomMenu = observer(function ({ model }: { model: MsaViewModel }) {
11
11
  return (
12
12
  <CascadingMenuButton
13
+ tooltip="Fit / zoom options"
13
14
  menuItems={[
14
15
  {
15
16
  label: 'Fit both vertically/horizontally',
@@ -37,16 +38,6 @@ const ZoomMenu = observer(function ({ model }: { model: MsaViewModel }) {
37
38
  model.resetZoom()
38
39
  },
39
40
  },
40
- {
41
- label: 'Scroll zoom',
42
- type: 'checkbox',
43
- checked: model.scrollZoom,
44
- onClick: () => {
45
- model.setScrollZoom(!model.scrollZoom)
46
- },
47
- helpText:
48
- 'When enabled, scrolling the mouse wheel zooms in and out without holding ctrl. Hold shift to pan instead. Ctrl/⌘+wheel always zooms.',
49
- },
50
41
  ]}
51
42
  >
52
43
  <MoreVert />
@@ -2,8 +2,16 @@ import type { MsaViewModel } from '../../model.ts'
2
2
  import type { MenuItem } from '@jbrowse/core/ui/Menu'
3
3
 
4
4
  export function msaSettingsMenuItems(model: MsaViewModel): MenuItem[] {
5
- const { drawMsaLetters, hideGaps, bgColor } = model
5
+ const { drawMsaLetters, hideGaps, bgColor, showColumnStats } = model
6
6
  return [
7
+ {
8
+ label: 'Show column statistics on hover',
9
+ type: 'checkbox',
10
+ checked: showColumnStats,
11
+ onClick: () => {
12
+ model.setShowColumnStats(!showColumnStats)
13
+ },
14
+ },
7
15
  {
8
16
  label: 'Draw letters',
9
17
  type: 'checkbox',
@@ -0,0 +1,54 @@
1
+ import React from 'react'
2
+
3
+ import { observer } from 'mobx-react'
4
+
5
+ import type { MsaViewModel } from '../../model.ts'
6
+
7
+ const pct = (n: number) => `${Math.round(n * 100)}%`
8
+
9
+ const ColumnStats = observer(function ({ model }: { model: MsaViewModel }) {
10
+ const stats = model.mouseOverColumnStats
11
+ if (!stats) {
12
+ return null
13
+ }
14
+ const {
15
+ col,
16
+ total,
17
+ conservation,
18
+ propertyConservation,
19
+ consensusLetter,
20
+ consensusFraction,
21
+ gapFraction,
22
+ distribution,
23
+ } = stats
24
+ const top = distribution.slice(0, 6)
25
+ return (
26
+ <div>
27
+ <b>Column {col + 1}</b> ({total} seqs)
28
+ <br />
29
+ Consensus: {consensusLetter || '-'} ({pct(consensusFraction)})
30
+ <br />
31
+ Conservation: {conservation.toFixed(2)}
32
+ {propertyConservation === undefined ? null : (
33
+ <>
34
+ <br />
35
+ Property conservation: {propertyConservation.toFixed(2)}
36
+ </>
37
+ )}
38
+ <br />
39
+ Gaps: {pct(gapFraction)}
40
+ {top.length > 0 ? (
41
+ <>
42
+ <br />
43
+ {top.map(([letter, count]) => (
44
+ <span key={letter} style={{ marginRight: 8 }}>
45
+ {letter} {pct(count / total)}
46
+ </span>
47
+ ))}
48
+ </>
49
+ ) : null}
50
+ </div>
51
+ )
52
+ })
53
+
54
+ export default ColumnStats
@@ -0,0 +1,78 @@
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 {
13
+ actuallyShowDomains,
14
+ visibleDomainTypes: visible,
15
+ fillPalette,
16
+ } = model
17
+ const ExpandIcon = expanded ? ExpandLess : ExpandMore
18
+
19
+ return actuallyShowDomains && visible.length > 0 ? (
20
+ <Paper
21
+ elevation={3}
22
+ style={{
23
+ position: 'absolute',
24
+ top: 4,
25
+ right: 4,
26
+ zIndex: 100,
27
+ maxWidth: 260,
28
+ maxHeight: '60%',
29
+ display: 'flex',
30
+ flexDirection: 'column',
31
+ opacity: 0.95,
32
+ }}
33
+ >
34
+ <div
35
+ style={{
36
+ display: 'flex',
37
+ alignItems: 'center',
38
+ justifyContent: 'flex-end',
39
+ padding: '2px 4px 2px 8px',
40
+ }}
41
+ >
42
+ <IconButton
43
+ size="small"
44
+ onClick={() => {
45
+ setExpanded(!expanded)
46
+ }}
47
+ >
48
+ <ExpandIcon fontSize="small" />
49
+ </IconButton>
50
+ </div>
51
+ {expanded ? (
52
+ <div style={{ overflow: 'auto', padding: '0 8px 6px' }}>
53
+ {visible.map(({ accession, name }) => (
54
+ <div
55
+ key={accession}
56
+ style={{ display: 'flex', alignItems: 'center', gap: 6 }}
57
+ title={accession}
58
+ >
59
+ <div
60
+ style={{
61
+ width: 12,
62
+ height: 12,
63
+ flexShrink: 0,
64
+ background: fillPalette[accession],
65
+ }}
66
+ />
67
+ <Typography variant="caption" noWrap>
68
+ {name}
69
+ </Typography>
70
+ </div>
71
+ ))}
72
+ </div>
73
+ ) : null}
74
+ </Paper>
75
+ ) : null
76
+ })
77
+
78
+ export default DomainLegend
@@ -1,11 +1,12 @@
1
- import React, { useEffect, useRef, useState } from 'react'
1
+ import React, { useState } from 'react'
2
2
 
3
3
  import { BaseTooltip } from '@jbrowse/core/ui'
4
- import { autorun } from 'mobx'
5
4
  import { observer } from 'mobx-react'
6
5
 
6
+ import ColumnStats from './ColumnStats.tsx'
7
7
  import { renderBoxFeatureCanvasBlock } from './renderBoxFeatureCanvasBlock.ts'
8
8
  import { renderMSABlock } from './renderMSABlock.ts'
9
+ import { useCanvasAutorun } from '../../useCanvasAutorun.ts'
9
10
  import { useColorContrast } from '../../useColorContrast.ts'
10
11
 
11
12
  import type { MsaViewModel } from '../../model.ts'
@@ -47,13 +48,8 @@ const MSACanvasBlock = observer(function ({
47
48
  } = model
48
49
  const { theme, contrastScheme } = useColorContrast(colorScheme)
49
50
 
50
- const ref = useRef<HTMLCanvasElement>(null)
51
- useEffect(() => {
52
- const ctx = ref.current?.getContext('2d')
53
- if (!ctx) {
54
- return
55
- }
56
- return autorun(() => {
51
+ const ref = useCanvasAutorun(
52
+ ctx => {
57
53
  ctx.resetTransform()
58
54
  ctx.clearRect(
59
55
  0,
@@ -77,19 +73,20 @@ const MSACanvasBlock = observer(function ({
77
73
  contrastScheme,
78
74
  model,
79
75
  })
80
- })
81
- }, [
82
- model,
83
- offsetX,
84
- offsetY,
85
- theme,
86
- blockSize,
87
- highResScaleFactor,
88
- contrastScheme,
89
- ])
76
+ },
77
+ [
78
+ model,
79
+ offsetX,
80
+ offsetY,
81
+ theme,
82
+ blockSize,
83
+ highResScaleFactor,
84
+ contrastScheme,
85
+ ],
86
+ )
90
87
 
91
88
  const [mousePosition, setMousePosition] = useState<{ x: number; y: number }>()
92
- const { hoveredInsertion } = model
89
+ const { hoveredInsertion, mouseOverDomains, showColumnStats } = model
93
90
 
94
91
  return (
95
92
  <>
@@ -116,12 +113,16 @@ const MSACanvasBlock = observer(function ({
116
113
  } else {
117
114
  model.setMousePos(undefined, undefined)
118
115
  }
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
116
+ // only track client coords when there's something to show in the
117
+ // tooltip: an insertion, a domain under the cursor, or column stats
118
+ // (when that option is enabled). avoids a re-render on every move
119
+ // when nothing would be shown
120
+ const hasTooltip =
121
+ model.hoveredInsertion ||
122
+ model.mouseOverDomains.length > 0 ||
123
+ (model.showColumnStats && !!model.mouseOverColumnStats)
121
124
  setMousePosition(
122
- model.hoveredInsertion
123
- ? { x: event.clientX, y: event.clientY }
124
- : undefined,
125
+ hasTooltip ? { x: event.clientX, y: event.clientY } : undefined,
125
126
  )
126
127
  }
127
128
  }}
@@ -167,6 +168,26 @@ const MSACanvasBlock = observer(function ({
167
168
  : hoveredInsertion.letters}
168
169
  </BaseTooltip>
169
170
  ) : null}
171
+ {!hoveredInsertion &&
172
+ mousePosition &&
173
+ (mouseOverDomains.length > 0 || showColumnStats) ? (
174
+ <BaseTooltip
175
+ clientPoint={{ x: mousePosition.x, y: mousePosition.y + 15 }}
176
+ >
177
+ {mouseOverDomains.map(d => (
178
+ <div key={`${d.accession}-${d.start}-${d.end}`}>
179
+ <b>{d.name}</b> ({d.accession}) {d.start}-{d.end}
180
+ {d.description ? (
181
+ <>
182
+ <br />
183
+ {d.description}
184
+ </>
185
+ ) : null}
186
+ </div>
187
+ ))}
188
+ {showColumnStats ? <ColumnStats model={model} /> : null}
189
+ </BaseTooltip>
190
+ ) : null}
170
191
  </>
171
192
  )
172
193
  })
@@ -1,10 +1,10 @@
1
- import React, { useEffect, useRef } from 'react'
1
+ import React from 'react'
2
2
 
3
3
  import { isAlive } from '@jbrowse/mobx-state-tree'
4
- import { autorun } from 'mobx'
5
4
  import { observer } from 'mobx-react'
6
5
 
7
6
  import { renderMouseover } from './renderMSAMouseover.ts'
7
+ import { useCanvasAutorun } from '../../useCanvasAutorun.ts'
8
8
 
9
9
  import type { MsaViewModel } from '../../model.ts'
10
10
 
@@ -13,20 +13,17 @@ const MSAMouseoverCanvas = observer(function ({
13
13
  }: {
14
14
  model: MsaViewModel
15
15
  }) {
16
- const ref = useRef<HTMLCanvasElement>(null)
17
16
  const { height, msaAreaWidth, verticalScrollbarWidth, highResScaleFactor } =
18
17
  model
19
18
  const width = msaAreaWidth - verticalScrollbarWidth
20
- useEffect(() => {
21
- const ctx = ref.current?.getContext('2d')
22
- return ctx
23
- ? autorun(() => {
24
- if (isAlive(model)) {
25
- renderMouseover({ ctx, model })
26
- }
27
- })
28
- : undefined
29
- }, [model])
19
+ const ref = useCanvasAutorun(
20
+ ctx => {
21
+ if (isAlive(model)) {
22
+ renderMouseover({ ctx, model })
23
+ }
24
+ },
25
+ [model],
26
+ )
30
27
 
31
28
  return (
32
29
  <canvas
@@ -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,95 @@ 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
92
+ ? segmentLabels.get(accession)
93
+ : undefined
94
+ if (label !== undefined && i === 0) {
95
+ const fontSize = Math.min(h - 2, 11)
96
+ ctx.font = `${fontSize}px sans-serif`
97
+ if (ctx.measureText(label).width + 2 <= lw) {
98
+ ctx.fillStyle = '#222'
99
+ ctx.textAlign = 'center'
100
+ ctx.textBaseline = 'middle'
101
+ ctx.fillText(label, x + lw / 2, t + h / 2)
102
+ }
103
+ }
104
+ } else {
105
+ drawGeneArrow({ ctx, x, t, w: lw, h, strand })
106
+ }
78
107
  }
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
108
  }
88
109
  }
89
110
  }
90
111
  }
112
+
113
+ // A gene arrow: a full-width rectangular body spanning the feature's
114
+ // start..end columns, plus a triangular head that points *beyond* that end in
115
+ // the strand direction (right for +, left for -). Keeping the body aligned to
116
+ // the exact start/end columns means + and - strand features read as having the
117
+ // same boundaries; the arrow lives outside them purely to show transcription
118
+ // direction, preserving the column-by-column homology down the rows.
119
+ function drawGeneArrow({
120
+ ctx,
121
+ x,
122
+ t,
123
+ w,
124
+ h,
125
+ strand,
126
+ }: {
127
+ ctx: RenderCtx
128
+ x: number
129
+ t: number
130
+ w: number
131
+ h: number
132
+ strand: number
133
+ }) {
134
+ // body spans bodyStart..bodyEnd (the exact columns); the head extends one
135
+ // row-height past bodyEnd in the strand direction
136
+ const dir = strand > 0 ? 1 : -1
137
+ const bodyStart = strand > 0 ? x : x + w
138
+ const bodyEnd = strand > 0 ? x + w : x
139
+ ctx.beginPath()
140
+ ctx.moveTo(bodyStart, t)
141
+ ctx.lineTo(bodyEnd, t)
142
+ ctx.lineTo(bodyEnd + dir * h, t + h / 2)
143
+ ctx.lineTo(bodyEnd, t + h)
144
+ ctx.lineTo(bodyStart, t + h)
145
+ ctx.closePath()
146
+ ctx.fill()
147
+ ctx.stroke()
148
+ }
@@ -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
@@ -1,9 +1,31 @@
1
+ import { multiRowHoverColor, referenceColor } from '../overlayColors.ts'
2
+
1
3
  import type { MsaViewModel } from '../../model.ts'
2
4
 
3
5
  const hoverColor = 'rgba(0,0,0,0.15)'
4
6
  const highlightColor = 'rgba(128,128,0,0.2)'
5
- const referenceColor = 'rgba(0,128,255,0.3)' // Blue highlight for reference row
6
- const multiRowHoverColor = 'rgba(255,165,0,0.15)' // Orange highlight for multi-row tree hover
7
+ // the persistent highlightColumns overlay: a stronger fill plus a solid border
8
+ // so a domain/motif band reads clearly over the colored alignment cells (the
9
+ // faint hover-style wash alone is invisible against clustalx coloring)
10
+ const highlightColumnsFill = 'rgba(255,140,0,0.28)'
11
+ const highlightColumnsBorder = 'rgba(210,90,0,0.95)'
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
+ }
7
29
 
8
30
  export function renderMouseover({
9
31
  ctx,
@@ -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
 
@@ -0,0 +1,12 @@
1
+ // Shared highlight colors for the MSA/tree hover and selection overlay canvases.
2
+ // Kept in one place so the reference-row color (drawn identically over both the
3
+ // tree and the alignment) can't drift between the two surfaces.
4
+
5
+ // reference (relativeTo) row, drawn over both the tree and the alignment
6
+ export const referenceColor = 'rgba(0,128,255,0.3)'
7
+
8
+ // tree-node descendant rows highlighted on hover. The alpha differs by surface
9
+ // intentionally: the tree background is plain so it can take a stronger wash,
10
+ // while the alignment cells are already colored so a lighter wash reads better.
11
+ export const treeHoverColor = 'rgba(255,165,0,0.2)'
12
+ export const multiRowHoverColor = 'rgba(255,165,0,0.15)'
@@ -26,5 +26,6 @@ export type RenderCtx = Pick<
26
26
  | 'strokeRect'
27
27
  | 'strokeStyle'
28
28
  | 'textAlign'
29
+ | 'textBaseline'
29
30
  | 'translate'
30
31
  >