react-msaview 5.10.0 → 6.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.
Files changed (226) hide show
  1. package/bundle/index.js +99 -99
  2. package/bundle/index.js.map +4 -4
  3. package/dist/clustalX.js +22 -22
  4. package/dist/clustalX.js.map +1 -1
  5. package/dist/columnCounts.d.ts +7 -0
  6. package/dist/columnCounts.js +18 -0
  7. package/dist/columnCounts.js.map +1 -1
  8. package/dist/components/CopyButton.d.ts +9 -0
  9. package/dist/components/CopyButton.js +23 -0
  10. package/dist/components/CopyButton.js.map +1 -0
  11. package/dist/components/DragHandle.d.ts +25 -0
  12. package/dist/components/DragHandle.js +49 -0
  13. package/dist/components/DragHandle.js.map +1 -0
  14. package/dist/components/Loading.js +2 -2
  15. package/dist/components/Loading.js.map +1 -1
  16. package/dist/components/MSAView.js +2 -2
  17. package/dist/components/MSAView.js.map +1 -1
  18. package/dist/components/ResizeHandles.d.ts +4 -2
  19. package/dist/components/ResizeHandles.js +35 -54
  20. package/dist/components/ResizeHandles.js.map +1 -1
  21. package/dist/components/SequenceTextArea.js +3 -10
  22. package/dist/components/SequenceTextArea.js.map +1 -1
  23. package/dist/components/VerticalScrollbar.js +11 -25
  24. package/dist/components/VerticalScrollbar.js.map +1 -1
  25. package/dist/components/dialogs/FeatureDialog.js +3 -3
  26. package/dist/components/dialogs/FeatureDialog.js.map +1 -1
  27. package/dist/components/dialogs/InterProScanDialog.js +52 -129
  28. package/dist/components/dialogs/InterProScanDialog.js.map +1 -1
  29. package/dist/components/dialogs/{UserProvidedDomainsDialog.d.ts → InterProScanFileDialog.d.ts} +2 -2
  30. package/dist/components/dialogs/{UserProvidedDomainsDialog.js → InterProScanFileDialog.js} +6 -10
  31. package/dist/components/dialogs/InterProScanFileDialog.js.map +1 -0
  32. package/dist/components/dialogs/TrackInfoDialog.d.ts +2 -4
  33. package/dist/components/dialogs/TrackInfoDialog.js +8 -14
  34. package/dist/components/dialogs/TrackInfoDialog.js.map +1 -1
  35. package/dist/components/header/FileMenu.js +3 -3
  36. package/dist/components/header/FileMenu.js.map +1 -1
  37. package/dist/components/header/GappynessSlider.js +1 -1
  38. package/dist/components/header/GappynessSlider.js.map +1 -1
  39. package/dist/components/header/{getDomainsMenu.d.ts → getAnnotationsMenu.d.ts} +1 -1
  40. package/dist/components/header/{getDomainsMenu.js → getAnnotationsMenu.js} +10 -9
  41. package/dist/components/header/getAnnotationsMenu.js.map +1 -0
  42. package/dist/components/header/settingsMenuItems.js +15 -0
  43. package/dist/components/header/settingsMenuItems.js.map +1 -1
  44. package/dist/components/import/ImportForm.js +1 -1
  45. package/dist/components/import/ImportForm.js.map +1 -1
  46. package/dist/components/import/ImportFormExamples.js +66 -57
  47. package/dist/components/import/ImportFormExamples.js.map +1 -1
  48. package/dist/components/minimap/Minimap.js +40 -23
  49. package/dist/components/minimap/Minimap.js.map +1 -1
  50. package/dist/components/minimap/MinimapSVG.d.ts +2 -1
  51. package/dist/components/minimap/MinimapSVG.js +3 -4
  52. package/dist/components/minimap/MinimapSVG.js.map +1 -1
  53. package/dist/components/minimap/minimapLayout.d.ts +10 -1
  54. package/dist/components/minimap/minimapLayout.js +15 -10
  55. package/dist/components/minimap/minimapLayout.js.map +1 -1
  56. package/dist/components/msa/{DomainLegend.d.ts → AnnotationLegend.d.ts} +2 -2
  57. package/dist/components/msa/{DomainLegend.js → AnnotationLegend.js} +4 -4
  58. package/dist/components/msa/AnnotationLegend.js.map +1 -0
  59. package/dist/components/msa/MSACanvas.js +9 -3
  60. package/dist/components/msa/MSACanvas.js.map +1 -1
  61. package/dist/components/msa/MSACanvasBlock.js +12 -3
  62. package/dist/components/msa/MSACanvasBlock.js.map +1 -1
  63. package/dist/components/msa/MSAMouseoverCanvas.js +1 -2
  64. package/dist/components/msa/MSAMouseoverCanvas.js.map +1 -1
  65. package/dist/components/msa/MSAPanel.js +2 -2
  66. package/dist/components/msa/MSAPanel.js.map +1 -1
  67. package/dist/components/msa/domainBandCursor.d.ts +17 -0
  68. package/dist/components/msa/domainBandCursor.js +39 -0
  69. package/dist/components/msa/domainBandCursor.js.map +1 -0
  70. package/dist/components/msa/msaRaster.d.ts +63 -0
  71. package/dist/components/msa/msaRaster.js +252 -0
  72. package/dist/components/msa/msaRaster.js.map +1 -0
  73. package/dist/components/msa/renderMSABlock.d.ts +2 -1
  74. package/dist/components/msa/renderMSABlock.js +27 -41
  75. package/dist/components/msa/renderMSABlock.js.map +1 -1
  76. package/dist/components/msa/renderMSAMouseover.js +1 -2
  77. package/dist/components/msa/renderMSAMouseover.js.map +1 -1
  78. package/dist/components/msa/tileColor.d.ts +6 -0
  79. package/dist/components/msa/tileColor.js +35 -0
  80. package/dist/components/msa/tileColor.js.map +1 -0
  81. package/dist/components/renderCtx.d.ts +1 -1
  82. package/dist/components/tracks/TrackBlocks.d.ts +14 -0
  83. package/dist/components/tracks/TrackBlocks.js +65 -0
  84. package/dist/components/tracks/TrackBlocks.js.map +1 -0
  85. package/dist/components/tracks/{renderTracksSvg.d.ts → drawTracks.d.ts} +22 -10
  86. package/dist/components/tracks/drawTracks.js +193 -0
  87. package/dist/components/tracks/drawTracks.js.map +1 -0
  88. package/dist/components/tree/TreeCanvas.js +8 -2
  89. package/dist/components/tree/TreeCanvas.js.map +1 -1
  90. package/dist/components/tree/TreeCanvasBlock.js +7 -4
  91. package/dist/components/tree/TreeCanvasBlock.js.map +1 -1
  92. package/dist/components/tree/TreeRuler.js +5 -2
  93. package/dist/components/tree/TreeRuler.js.map +1 -1
  94. package/dist/components/tree/renderTreeCanvas.js +5 -2
  95. package/dist/components/tree/renderTreeCanvas.js.map +1 -1
  96. package/dist/constants.d.ts +3 -0
  97. package/dist/constants.js +8 -0
  98. package/dist/constants.js.map +1 -1
  99. package/dist/ggplotPalettes.d.ts +0 -1
  100. package/dist/ggplotPalettes.js +0 -3
  101. package/dist/ggplotPalettes.js.map +1 -1
  102. package/dist/hierarchy.d.ts +2 -6
  103. package/dist/hierarchy.js +4 -6
  104. package/dist/hierarchy.js.map +1 -1
  105. package/dist/index.d.ts +2 -2
  106. package/dist/launchInterProScan.d.ts +0 -23
  107. package/dist/launchInterProScan.js +2 -5
  108. package/dist/launchInterProScan.js.map +1 -1
  109. package/dist/measureTextCanvas.js +21 -6
  110. package/dist/measureTextCanvas.js.map +1 -1
  111. package/dist/model/DataModel.d.ts +1 -4
  112. package/dist/model/DataModel.js +8 -10
  113. package/dist/model/DataModel.js.map +1 -1
  114. package/dist/model.d.ts +86 -107
  115. package/dist/model.js +238 -162
  116. package/dist/model.js.map +1 -1
  117. package/dist/neighborJoining.js +34 -18
  118. package/dist/neighborJoining.js.map +1 -1
  119. package/dist/renderTestEnv.d.ts +43 -0
  120. package/dist/renderTestEnv.js +88 -0
  121. package/dist/renderTestEnv.js.map +1 -0
  122. package/dist/renderToSvg.js +3 -3
  123. package/dist/renderToSvg.js.map +1 -1
  124. package/dist/rowCoordinateCalculations.d.ts +13 -17
  125. package/dist/rowCoordinateCalculations.js +45 -39
  126. package/dist/rowCoordinateCalculations.js.map +1 -1
  127. package/dist/sequenceLogo.d.ts +28 -0
  128. package/dist/sequenceLogo.js +47 -0
  129. package/dist/sequenceLogo.js.map +1 -0
  130. package/dist/types.d.ts +6 -11
  131. package/dist/version.d.ts +1 -1
  132. package/dist/version.js +1 -1
  133. package/dist/version.js.map +1 -1
  134. package/package.json +5 -5
  135. package/src/clustalX.ts +22 -22
  136. package/src/colSpace.test.ts +27 -0
  137. package/src/columnCounts.test.ts +15 -0
  138. package/src/columnCounts.ts +19 -0
  139. package/src/components/CopyButton.tsx +44 -0
  140. package/src/components/DragHandle.tsx +81 -0
  141. package/src/components/Loading.tsx +10 -1
  142. package/src/components/MSAView.tsx +2 -3
  143. package/src/components/ResizeHandles.tsx +70 -87
  144. package/src/components/SequenceTextArea.tsx +3 -16
  145. package/src/components/VerticalScrollbar.tsx +20 -33
  146. package/src/components/dialogs/FeatureDialog.tsx +3 -4
  147. package/src/components/dialogs/InterProScanDialog.tsx +54 -131
  148. package/src/components/dialogs/{UserProvidedDomainsDialog.tsx → InterProScanFileDialog.tsx} +6 -14
  149. package/src/components/dialogs/TrackInfoDialog.tsx +18 -20
  150. package/src/components/header/FileMenu.tsx +3 -3
  151. package/src/components/header/GappynessSlider.tsx +4 -1
  152. package/src/components/header/{getDomainsMenu.ts → getAnnotationsMenu.ts} +10 -9
  153. package/src/components/header/settingsMenuItems.ts +18 -0
  154. package/src/components/import/ImportForm.tsx +1 -1
  155. package/src/components/import/ImportFormExamples.tsx +82 -133
  156. package/src/components/minimap/Minimap.tsx +67 -32
  157. package/src/components/minimap/MinimapSVG.tsx +32 -31
  158. package/src/components/minimap/minimapLayout.test.ts +60 -0
  159. package/src/components/minimap/minimapLayout.ts +15 -10
  160. package/src/components/msa/{DomainLegend.tsx → AnnotationLegend.tsx} +7 -3
  161. package/src/components/msa/MSACanvas.tsx +24 -11
  162. package/src/components/msa/MSACanvasBlock.tsx +13 -3
  163. package/src/components/msa/MSAMouseoverCanvas.tsx +1 -3
  164. package/src/components/msa/MSAPanel.tsx +2 -2
  165. package/src/components/msa/domainBandCursor.ts +41 -0
  166. package/src/components/msa/msaRaster.test.ts +145 -0
  167. package/src/components/msa/msaRaster.ts +350 -0
  168. package/src/components/msa/renderMSABlock.ts +34 -43
  169. package/src/components/msa/renderMSAMouseover.ts +1 -3
  170. package/src/components/msa/tileColor.ts +37 -0
  171. package/src/components/renderCtx.ts +2 -0
  172. package/src/components/tracks/TrackBlocks.tsx +111 -0
  173. package/src/components/tracks/drawTracks.ts +323 -0
  174. package/src/components/tree/TreeCanvas.tsx +16 -4
  175. package/src/components/tree/TreeCanvasBlock.tsx +18 -11
  176. package/src/components/tree/TreeRuler.tsx +7 -2
  177. package/src/components/tree/renderTreeCanvas.test.ts +15 -7
  178. package/src/components/tree/renderTreeCanvas.ts +5 -1
  179. package/src/constants.ts +10 -0
  180. package/src/domainBands.test.ts +24 -0
  181. package/src/domainLetterColor.test.ts +80 -0
  182. package/src/ggplotPalettes.ts +0 -4
  183. package/src/hierarchy.test.ts +7 -7
  184. package/src/hierarchy.ts +6 -9
  185. package/src/impgRender.test.tsx +2 -57
  186. package/src/index.ts +4 -1
  187. package/src/largeTree.test.ts +39 -4
  188. package/src/launchInterProScan.ts +3 -30
  189. package/src/loadErrorDisplay.test.tsx +102 -0
  190. package/src/measureTextCanvas.ts +22 -6
  191. package/src/model/DataModel.ts +11 -11
  192. package/src/model.ts +262 -172
  193. package/src/neighborJoining.ts +36 -18
  194. package/src/renderDomainsSvg.test.tsx +2 -57
  195. package/src/renderMinimapSvg.test.tsx +55 -0
  196. package/src/renderSequenceLogoSvg.test.tsx +176 -0
  197. package/src/renderTestEnv.ts +96 -0
  198. package/src/renderToSvg.tsx +3 -3
  199. package/src/rowCoordinateCalculations.ts +48 -44
  200. package/src/sequenceLogo.test.ts +88 -0
  201. package/src/sequenceLogo.ts +63 -0
  202. package/src/types.ts +17 -16
  203. package/src/version.ts +1 -1
  204. package/src/viewChrome.test.tsx +134 -0
  205. package/dist/components/ConservationTrack.d.ts +0 -8
  206. package/dist/components/ConservationTrack.js +0 -49
  207. package/dist/components/ConservationTrack.js.map +0 -1
  208. package/dist/components/TextTrack.d.ts +0 -8
  209. package/dist/components/TextTrack.js +0 -58
  210. package/dist/components/TextTrack.js.map +0 -1
  211. package/dist/components/dialogs/DomainDialog.d.ts +0 -6
  212. package/dist/components/dialogs/DomainDialog.js +0 -19
  213. package/dist/components/dialogs/DomainDialog.js.map +0 -1
  214. package/dist/components/dialogs/UserProvidedDomainsDialog.js.map +0 -1
  215. package/dist/components/header/SettingsMenu.d.ts +0 -6
  216. package/dist/components/header/SettingsMenu.js +0 -40
  217. package/dist/components/header/SettingsMenu.js.map +0 -1
  218. package/dist/components/header/getDomainsMenu.js.map +0 -1
  219. package/dist/components/msa/DomainLegend.js.map +0 -1
  220. package/dist/components/tracks/renderTracksSvg.js +0 -126
  221. package/dist/components/tracks/renderTracksSvg.js.map +0 -1
  222. package/src/components/ConservationTrack.tsx +0 -99
  223. package/src/components/TextTrack.tsx +0 -103
  224. package/src/components/dialogs/DomainDialog.tsx +0 -45
  225. package/src/components/header/SettingsMenu.tsx +0 -57
  226. package/src/components/tracks/renderTracksSvg.ts +0 -247
@@ -7,7 +7,7 @@ import Menu from '@mui/icons-material/Menu'
7
7
  import PhotoCamera from '@mui/icons-material/PhotoCamera'
8
8
  import { observer } from 'mobx-react'
9
9
 
10
- import { getDomainMenu } from './getDomainsMenu.ts'
10
+ import { getAnnotationsMenu } from './getAnnotationsMenu.ts'
11
11
 
12
12
  import type { MsaViewModel } from '../../model.ts'
13
13
 
@@ -41,9 +41,9 @@ const FileMenu = observer(({ model }: { model: MsaViewModel }) => {
41
41
  },
42
42
  },
43
43
  {
44
- label: 'Domain settings',
44
+ label: 'Annotations',
45
45
  type: 'subMenu',
46
- subMenu: getDomainMenu({
46
+ subMenu: getAnnotationsMenu({
47
47
  model,
48
48
  }),
49
49
  },
@@ -16,8 +16,11 @@ const GappynessSlider = observer(function GappynessSlider({
16
16
  }
17
17
  return (
18
18
  <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
19
+ {/* the threshold is inclusive: blanks are columns whose gap count is at
20
+ least ceil(allowedGappyness% of the rows), so at 100 it hides the
21
+ columns that are entirely gaps rather than nothing */}
19
22
  <Typography style={{ whiteSpace: 'nowrap' }}>
20
- Hide columns w/ &gt;{allowedGappyness}% gaps
23
+ Hide columns w/ &ge;{allowedGappyness}% gaps
21
24
  </Typography>
22
25
  <Slider
23
26
  // Named, because the class alone is not enough to find it from outside.
@@ -9,22 +9,23 @@ import Visibility from '@mui/icons-material/Visibility'
9
9
  import type { MsaViewModel } from '../../model.ts'
10
10
 
11
11
  const FeatureFilterDialog = lazy(() => import('../dialogs/FeatureDialog.tsx'))
12
- const UserProvidedDomainsDialog = lazy(
13
- () => import('../dialogs/UserProvidedDomainsDialog.tsx'),
12
+ const InterProScanFileDialog = lazy(
13
+ () => import('../dialogs/InterProScanFileDialog.tsx'),
14
14
  )
15
15
  const InterProScanDialog = lazy(
16
16
  () => import('../dialogs/InterProScanDialog.tsx'),
17
17
  )
18
18
 
19
- export function getDomainMenu({ model }: { model: MsaViewModel }) {
19
+ export function getAnnotationsMenu({ model }: { model: MsaViewModel }) {
20
20
  const { showDomains, actuallyShowDomains, subFeatureRows, noDomains } = model
21
+ const noneLoaded = noDomains ? ' (none loaded)' : ''
21
22
  return [
22
23
  {
23
- label: 'Open domains...',
24
+ label: 'Open InterProScan results...',
24
25
  icon: FolderOpen,
25
26
  onClick: () => {
26
27
  model.queueDialog(handleClose => [
27
- UserProvidedDomainsDialog,
28
+ InterProScanFileDialog,
28
29
  {
29
30
  handleClose,
30
31
  model,
@@ -33,7 +34,7 @@ export function getDomainMenu({ model }: { model: MsaViewModel }) {
33
34
  },
34
35
  },
35
36
  {
36
- label: 'Query InterProScan for domains...',
37
+ label: 'Query InterProScan...',
37
38
  icon: Search,
38
39
  onClick: () => {
39
40
  model.queueDialog(handleClose => [
@@ -46,7 +47,7 @@ export function getDomainMenu({ model }: { model: MsaViewModel }) {
46
47
  },
47
48
  },
48
49
  {
49
- label: `Show domains${noDomains ? ' (no domains loaded)' : ''}`,
50
+ label: `Show annotations${noneLoaded}`,
50
51
  disabled: noDomains,
51
52
  icon: Visibility,
52
53
  checked: actuallyShowDomains,
@@ -56,7 +57,7 @@ export function getDomainMenu({ model }: { model: MsaViewModel }) {
56
57
  },
57
58
  },
58
59
  {
59
- label: `Use sub-row layout${noDomains ? ' (no domains loaded)' : ''}`,
60
+ label: `Use sub-row layout${noneLoaded}`,
60
61
  disabled: noDomains,
61
62
  checked: actuallyShowDomains ? subFeatureRows : false,
62
63
  icon: Sort,
@@ -66,7 +67,7 @@ export function getDomainMenu({ model }: { model: MsaViewModel }) {
66
67
  },
67
68
  },
68
69
  {
69
- label: `Filter domains${noDomains ? ' (no domains loaded)' : ''}`,
70
+ label: `Filter annotations${noneLoaded}`,
70
71
  icon: FilterAlt,
71
72
  disabled: noDomains,
72
73
  onClick: () => {
@@ -12,9 +12,27 @@ function toggle(label: string, checked: boolean, set: (arg: boolean) => void) {
12
12
  }
13
13
  }
14
14
 
15
+ // Every track the alignment can show, with its current state. Without this the
16
+ // "Close" item on a track's own dropdown is a one-way door -- there is nowhere
17
+ // else in the UI to turn a track back on.
18
+ function tracksSubMenu(model: MsaViewModel): MenuItem[] {
19
+ const shown = new Set(model.turnedOnTracks.map(t => t.model.id))
20
+ return model.tracks.map(({ model: { id, name } }) =>
21
+ toggle(name, shown.has(id), () => {
22
+ model.toggleTrack(id)
23
+ }),
24
+ )
25
+ }
26
+
15
27
  export function msaSettingsMenuItems(model: MsaViewModel): MenuItem[] {
16
28
  const { drawMsaLetters, hideGaps, bgColor, showColumnStats } = model
17
29
  return [
30
+ {
31
+ label: 'Tracks',
32
+ type: 'subMenu' as const,
33
+ subMenu: tracksSubMenu(model),
34
+ },
35
+ { type: 'divider' as const },
18
36
  toggle('Show column statistics on hover', showColumnStats, arg => {
19
37
  model.setShowColumnStats(arg)
20
38
  }),
@@ -50,7 +50,7 @@ const ImportForm = observer(function ({ model }: { model: MsaViewModel }) {
50
50
  <FileSelector location={treeFile} setLocation={setTreeFile} />
51
51
  </div>
52
52
  <div>
53
- <Typography>InterProScan GFF file or URL (optional)</Typography>
53
+ <Typography>Annotation GFF file or URL (optional)</Typography>
54
54
  <FileSelector location={gffFile} setLocation={setGffFile} />
55
55
  </div>
56
56
  </div>
@@ -8,149 +8,98 @@ import { load } from './util.ts'
8
8
 
9
9
  import type { MsaViewModel } from '../../model.ts'
10
10
 
11
- function ListItem({
12
- onClick,
13
- model,
14
- children,
15
- }: {
16
- onClick: () => void
17
- model: MsaViewModel
18
- children: React.ReactNode
19
- }) {
20
- return (
21
- <li>
22
- <Link
23
- onClick={event => {
24
- model.setError(undefined)
25
- event.preventDefault()
26
- onClick()
27
- }}
28
- href="#"
29
- >
30
- <Typography component="span">{children}</Typography>
31
- </Link>
32
- </li>
33
- )
11
+ const BASE = 'https://jbrowse.org/genomes/multiple_sequence_alignments'
12
+ const TREES = 'https://jbrowse.org/genomes/newicktrees'
13
+
14
+ // each example either points at remote files (msa/tree/gff urls) or carries its
15
+ // data inline, which is what the two bundled small ones do
16
+ interface Example {
17
+ label: string
18
+ msa?: string
19
+ tree?: string
20
+ gff?: string
21
+ inline?: { msa: string; tree?: string }
34
22
  }
35
23
 
36
- const BASE = 'https://jbrowse.org/genomes/multiple_sequence_alignments'
24
+ const examples: Example[] = [
25
+ {
26
+ label: '230k COVID-19 samples (tree only)',
27
+ tree: `${TREES}/sarscov2phylo.pub.ft.nh`,
28
+ },
29
+ {
30
+ label: 'Small protein MSA+tree',
31
+ inline: { msa: smallMSA, tree: smallTree },
32
+ },
33
+ {
34
+ label: 'Small MSA only',
35
+ inline: { msa: smallMSAOnly },
36
+ },
37
+ {
38
+ label: 'PFAM SARS-CoV2 multi-stockholm',
39
+ msa: `${BASE}/pfam-cov2.stock`,
40
+ },
41
+ {
42
+ label: 'PFAM SARS-CoV2 multi-stockholm w/ domains loaded',
43
+ msa: `${BASE}/pfam-cov2.stock`,
44
+ gff: `${BASE}/pfam-cov2-domains.gff`,
45
+ },
46
+ {
47
+ label: 'Lysine stockholm file',
48
+ msa: `${BASE}/Lysine.stock`,
49
+ },
50
+ {
51
+ label: 'PF01601 stockholm file (SARS-CoV2 spike protein)',
52
+ msa: `${BASE}/PF01601_full.txt`,
53
+ },
54
+ {
55
+ label: 'Europe COVID full genomes (LR883044.1 and 199 other sequences)',
56
+ msa: `${BASE}/europe_covid.fa`,
57
+ },
58
+ {
59
+ label: 'MAFFT+VeryFastTree(17.9k samples)',
60
+ msa: `${BASE}/rhv_test-only.aligned_with_mafft_auto.fa`,
61
+ tree: `${BASE}/rhv_test-only.aligned_with_mafft_auto.nh`,
62
+ },
63
+ {
64
+ label: 'Human BLAST results mfa',
65
+ msa: 'https://jbrowse.org/demos/ttc39a.mfa',
66
+ },
67
+ ]
68
+
69
+ function uri(url?: string) {
70
+ return url ? { uri: url, locationType: 'UriLocation' as const } : undefined
71
+ }
37
72
 
38
73
  const ImportFormExamples = observer(function ({
39
74
  model,
40
75
  }: {
41
76
  model: MsaViewModel
42
77
  }) {
43
- function loadUris({
44
- msa,
45
- tree,
46
- gff,
47
- }: {
48
- msa?: string
49
- tree?: string
50
- gff?: string
51
- }) {
52
- try {
53
- load(
54
- model,
55
- msa ? { uri: msa, locationType: 'UriLocation' } : undefined,
56
- tree ? { uri: tree, locationType: 'UriLocation' } : undefined,
57
- gff ? { uri: gff, locationType: 'UriLocation' } : undefined,
58
- )
59
- } catch (e) {
60
- console.error(e)
61
- model.setError(e)
62
- }
63
- }
64
78
  return (
65
79
  <ul>
66
- <ListItem
67
- model={model}
68
- onClick={() => {
69
- loadUris({
70
- tree: 'https://jbrowse.org/genomes/newicktrees/sarscov2phylo.pub.ft.nh',
71
- })
72
- }}
73
- >
74
- 230k COVID-19 samples (tree only)
75
- </ListItem>
76
- <ListItem
77
- model={model}
78
- onClick={() => {
79
- model.setData({ msa: smallMSA, tree: smallTree })
80
- }}
81
- >
82
- Small protein MSA+tree
83
- </ListItem>
84
- <ListItem
85
- model={model}
86
- onClick={() => {
87
- model.setData({ msa: smallMSAOnly })
88
- }}
89
- >
90
- Small MSA only
91
- </ListItem>
92
- <ListItem
93
- model={model}
94
- onClick={() => {
95
- loadUris({ msa: `${BASE}/pfam-cov2.stock` })
96
- }}
97
- >
98
- PFAM SARS-CoV2 multi-stockholm
99
- </ListItem>
100
- <ListItem
101
- model={model}
102
- onClick={() => {
103
- loadUris({
104
- msa: `${BASE}/pfam-cov2.stock`,
105
- gff: `${BASE}/pfam-cov2-domains.gff`,
106
- })
107
- }}
108
- >
109
- PFAM SARS-CoV2 multi-stockholm w/ domains loaded
110
- </ListItem>
111
- <ListItem
112
- model={model}
113
- onClick={() => {
114
- loadUris({ msa: `${BASE}/Lysine.stock` })
115
- }}
116
- >
117
- Lysine stockholm file
118
- </ListItem>
119
- <ListItem
120
- model={model}
121
- onClick={() => {
122
- loadUris({ msa: `${BASE}/PF01601_full.txt` })
123
- }}
124
- >
125
- PF01601 stockholm file (SARS-CoV2 spike protein)
126
- </ListItem>
127
- <ListItem
128
- model={model}
129
- onClick={() => {
130
- loadUris({ msa: `${BASE}/europe_covid.fa` })
131
- }}
132
- >
133
- Europe COVID full genomes (LR883044.1 and 199 other sequences)
134
- </ListItem>
135
- <ListItem
136
- model={model}
137
- onClick={() => {
138
- loadUris({
139
- msa: `${BASE}/rhv_test-only.aligned_with_mafft_auto.fa`,
140
- tree: `${BASE}/rhv_test-only.aligned_with_mafft_auto.nh`,
141
- })
142
- }}
143
- >
144
- MAFFT+VeryFastTree(17.9k samples)
145
- </ListItem>
146
- <ListItem
147
- model={model}
148
- onClick={() => {
149
- loadUris({ msa: 'https://jbrowse.org/demos/ttc39a.mfa' })
150
- }}
151
- >
152
- Human BLAST results mfa
153
- </ListItem>
80
+ {examples.map(({ label, msa, tree, gff, inline }) => (
81
+ <li key={label}>
82
+ <Link
83
+ href="#"
84
+ onClick={event => {
85
+ event.preventDefault()
86
+ model.setError(undefined)
87
+ try {
88
+ if (inline) {
89
+ model.setData(inline)
90
+ } else {
91
+ load(model, uri(msa), uri(tree), uri(gff))
92
+ }
93
+ } catch (e) {
94
+ console.error(e)
95
+ model.setError(e)
96
+ }
97
+ }}
98
+ >
99
+ <Typography component="span">{label}</Typography>
100
+ </Link>
101
+ </li>
102
+ ))}
154
103
  </ul>
155
104
  )
156
105
  })
@@ -1,67 +1,102 @@
1
- import React, { useCallback, useState } from 'react'
1
+ import React, { useCallback } from 'react'
2
2
 
3
+ import { useTheme } from '@mui/material'
3
4
  import { observer } from 'mobx-react'
4
5
 
5
- import { useDragScroll } from '../../useDragScroll.ts'
6
+ import { useCanvasAutorun } from '../../useCanvasAutorun.ts'
7
+ import DragHandle, { scrollbarThumbFill } from '../DragHandle.tsx'
8
+ import { msaThumbnail } from '../msa/msaRaster.ts'
6
9
  import { MINIMAP_BAR_HEIGHT, getMinimapLayout } from './minimapLayout.ts'
7
10
 
8
11
  import type { MsaViewModel } from '../../model.ts'
9
12
 
10
13
  const Minimap = observer(function ({ model }: { model: MsaViewModel }) {
11
- const [hovered, setHovered] = useState(false)
12
- const { minimapHeight } = model
13
- const { unit, s, w, polygonHeight, polygonPoints } = getMinimapLayout(model)
14
- const fill = 'rgba(66, 119, 127, 0.3)'
14
+ const { minimapHeight, msaCanvasWidth, highResScaleFactor } = model
15
+ const theme = useTheme()
16
+ const { unit, s, w, polygonHeight, polygonPoints } = getMinimapLayout(
17
+ model,
18
+ msaCanvasWidth,
19
+ )
20
+
21
+ // the whole alignment, sampled down to the bar: it says where the conserved
22
+ // blocks and the gappy stretches are, which is what a scroll aims at
23
+ const barWidth = Math.round(msaCanvasWidth * highResScaleFactor)
24
+ const barHeight = Math.round(MINIMAP_BAR_HEIGHT * highResScaleFactor)
25
+ const ref = useCanvasAutorun({
26
+ draw: ctx => {
27
+ ctx.resetTransform()
28
+ ctx.clearRect(0, 0, barWidth, barHeight)
29
+ const thumbnail = msaThumbnail({
30
+ model,
31
+ theme,
32
+ height: MINIMAP_BAR_HEIGHT,
33
+ })
34
+ if (thumbnail) {
35
+ ctx.imageSmoothingEnabled = true
36
+ ctx.drawImage(
37
+ thumbnail,
38
+ 0,
39
+ 0,
40
+ thumbnail.width,
41
+ thumbnail.height,
42
+ 0,
43
+ 0,
44
+ barWidth,
45
+ barHeight,
46
+ )
47
+ }
48
+ },
49
+ width: barWidth,
50
+ height: barHeight,
51
+ deps: [model, theme],
52
+ })
15
53
 
16
- const { startDrag } = useDragScroll(
17
- 'x',
18
- useCallback(
19
- (delta: number, startScroll: number) => {
20
- model.setScrollX(startScroll - delta / unit)
21
- },
22
- [model, unit],
23
- ),
54
+ const onDrag = useCallback(
55
+ (delta: number, startScroll: number) => {
56
+ model.setScrollX(startScroll - delta / unit)
57
+ },
58
+ [model, unit],
24
59
  )
25
60
 
26
61
  return (
62
+ // sized and placed to match the alignment canvas exactly: a minimap that
63
+ // spans a different width than the columns it maps puts its viewport
64
+ // rectangle over the wrong ones
27
65
  <div
28
66
  style={{
29
67
  position: 'relative',
30
68
  height: minimapHeight,
31
- width: '100%',
69
+ width: msaCanvasWidth,
70
+ flexShrink: 0,
32
71
  }}
33
72
  >
34
- <div
73
+ <canvas
74
+ ref={ref}
75
+ width={barWidth}
76
+ height={barHeight}
35
77
  style={{
78
+ display: 'block',
79
+ width: msaCanvasWidth,
36
80
  height: MINIMAP_BAR_HEIGHT,
37
81
  boxSizing: 'border-box',
38
82
  border: '1px solid #555',
39
83
  }}
40
84
  />
41
- <div
85
+ <DragHandle
86
+ axis="x"
87
+ variant="thumb"
88
+ getStart={() => model.scrollX}
89
+ onDrag={onDrag}
42
90
  style={{
43
- position: 'absolute',
44
91
  top: 0,
45
92
  left: Math.max(0, s),
46
- background: hovered ? 'rgba(66,119,127,0.6)' : fill,
47
- cursor: 'pointer',
48
93
  height: MINIMAP_BAR_HEIGHT,
49
94
  width: w,
50
- zIndex: 100,
51
- }}
52
- onMouseOver={() => {
53
- setHovered(true)
54
- }}
55
- onMouseOut={() => {
56
- setHovered(false)
57
- }}
58
- onMouseDown={event => {
59
- startDrag(event, model.scrollX)
60
95
  }}
61
96
  />
62
97
 
63
- <svg height={polygonHeight} style={{ width: '100%' }}>
64
- <polygon fill={fill} points={polygonPoints} />
98
+ <svg height={polygonHeight} width={msaCanvasWidth}>
99
+ <polygon fill={scrollbarThumbFill} points={polygonPoints} />
65
100
  </svg>
66
101
  </div>
67
102
  )
@@ -6,40 +6,41 @@ import { MINIMAP_BAR_HEIGHT, getMinimapLayout } from './minimapLayout.ts'
6
6
 
7
7
  import type { MsaViewModel } from '../../model.ts'
8
8
 
9
- const MinimapSVG = observer(({ model }: { model: MsaViewModel }) => {
10
- const { msaAreaWidth } = model
11
- const { s, w, polygonPoints } = getMinimapLayout(model)
12
- const fillColor = 'rgb(66, 119, 127)'
13
- const fillOpacity = 0.3
9
+ const MinimapSVG = observer(
10
+ ({ model, width }: { model: MsaViewModel; width: number }) => {
11
+ const { s, w, polygonPoints } = getMinimapLayout(model, width)
12
+ const fillColor = 'rgb(66, 119, 127)'
13
+ const fillOpacity = 0.3
14
14
 
15
- return (
16
- <>
17
- <rect
18
- x={0}
19
- y={0}
20
- width={msaAreaWidth}
21
- height={MINIMAP_BAR_HEIGHT}
22
- stroke="#555"
23
- fill="none"
24
- />
25
- <rect
26
- x={Math.max(0, s)}
27
- y={0}
28
- width={w}
29
- height={MINIMAP_BAR_HEIGHT}
30
- fill={fillColor}
31
- fillOpacity={fillOpacity}
32
- stroke="#555"
33
- />
34
- <g transform={`translate(0 ${MINIMAP_BAR_HEIGHT})`}>
35
- <polygon
15
+ return (
16
+ <>
17
+ <rect
18
+ x={0}
19
+ y={0}
20
+ width={width}
21
+ height={MINIMAP_BAR_HEIGHT}
22
+ stroke="#555"
23
+ fill="none"
24
+ />
25
+ <rect
26
+ x={Math.max(0, s)}
27
+ y={0}
28
+ width={w}
29
+ height={MINIMAP_BAR_HEIGHT}
36
30
  fill={fillColor}
37
31
  fillOpacity={fillOpacity}
38
- points={polygonPoints}
32
+ stroke="#555"
39
33
  />
40
- </g>
41
- </>
42
- )
43
- })
34
+ <g transform={`translate(0 ${MINIMAP_BAR_HEIGHT})`}>
35
+ <polygon
36
+ fill={fillColor}
37
+ fillOpacity={fillOpacity}
38
+ points={polygonPoints}
39
+ />
40
+ </g>
41
+ </>
42
+ )
43
+ },
44
+ )
44
45
 
45
46
  export default MinimapSVG
@@ -0,0 +1,60 @@
1
+ // @vitest-environment jsdom
2
+ import { expect, test } from 'vitest'
3
+
4
+ import MSAModelF from '../../model.ts'
5
+ import { MINIMAP_BAR_HEIGHT, getMinimapLayout } from './minimapLayout.ts'
6
+
7
+ // wide enough to overflow horizontally, tall enough to raise the vertical
8
+ // scrollbar — which takes 20px out of the width the alignment actually draws on
9
+ function makeModel() {
10
+ const names = Array.from({ length: 60 }, (_, i) => `s${i}`)
11
+ const model = MSAModelF().create({
12
+ type: 'MsaView',
13
+ msaFormat: 'fasta',
14
+ height: 400,
15
+ data: { msa: names.map(n => `>${n}\n${'ACGT'.repeat(50)}`).join('\n') },
16
+ })
17
+ model.setWidth(1000)
18
+ return model
19
+ }
20
+
21
+ test('the viewport rectangle scales by the canvas width, not the msa area', () => {
22
+ const model = makeModel()
23
+ expect(model.showVerticalScrollbar).toBe(true)
24
+ expect(model.msaCanvasWidth).toBe(model.msaAreaWidth - 20)
25
+
26
+ const { unit, s, w } = getMinimapLayout(model, model.msaCanvasWidth)
27
+ expect(unit).toBeCloseTo(model.msaCanvasWidth / model.totalWidth)
28
+ // unscrolled, the rectangle starts at the left edge and covers the fraction
29
+ // of the alignment on screen. Sizing it by msaAreaWidth instead overstated
30
+ // that fraction by the scrollbar's width.
31
+ expect(s).toBeCloseTo(0)
32
+ expect(w).toBeCloseTo(
33
+ (model.msaCanvasWidth / model.totalWidth) * model.msaCanvasWidth,
34
+ )
35
+ })
36
+
37
+ test('the rectangle tracks scrollX and the trapezoid spans the given width', () => {
38
+ const model = makeModel()
39
+ model.setScrollX(-300)
40
+
41
+ const width = 400
42
+ const { s, polygonHeight, polygonPoints } = getMinimapLayout(model, width)
43
+ expect(s).toBeCloseTo((300 / model.totalWidth) * width)
44
+ expect(polygonHeight).toBe(model.minimapHeight - MINIMAP_BAR_HEIGHT)
45
+ // bottom edge spans the full minimap, top edge is the viewport rectangle
46
+ expect(
47
+ polygonPoints.endsWith(`0,${polygonHeight} ${width},${polygonHeight}`),
48
+ ).toBe(true)
49
+ })
50
+
51
+ test('an empty alignment collapses to a zero-width unit instead of dividing by zero', () => {
52
+ const model = MSAModelF().create({ type: 'MsaView' })
53
+ model.setWidth(1000)
54
+ expect(model.totalWidth).toBe(0)
55
+ const { unit, s, w } = getMinimapLayout(model, 400)
56
+ expect(unit).toBe(0)
57
+ expect(s).toBeCloseTo(0)
58
+ // still clamped to the minimum grabbable width
59
+ expect(w).toBe(20)
60
+ })
@@ -2,16 +2,21 @@ import type { MsaViewModel } from '../../model.ts'
2
2
 
3
3
  export const MINIMAP_BAR_HEIGHT = 12
4
4
 
5
- // viewport-rectangle and trapezoid geometry shared by the interactive Minimap
6
- // and the static MinimapSVG (used for SVG export)
7
- export function getMinimapLayout(model: MsaViewModel) {
8
- const { scrollX, msaAreaWidth, minimapHeight, colWidth, numColumns } = model
9
- const unit = msaAreaWidth / numColumns / colWidth
10
- const left = -scrollX
11
- const s = left * unit
12
- const e = (left + msaAreaWidth) * unit
13
- const w = Math.max(e - s, 20)
5
+ /**
6
+ * Viewport-rectangle and trapezoid geometry for a minimap drawn `minimapWidth`
7
+ * pixels wide, shared by the interactive Minimap and the static MinimapSVG.
8
+ *
9
+ * The width is a parameter rather than read off the model because the two
10
+ * differ: on screen the minimap sits over the alignment canvas, while the SVG
11
+ * export lays the whole figure out on its own grid. What the rectangle *marks*
12
+ * is always the on-screen viewport, so that part comes from the model.
13
+ */
14
+ export function getMinimapLayout(model: MsaViewModel, minimapWidth: number) {
15
+ const { scrollX, minimapHeight, totalWidth, msaCanvasWidth } = model
16
+ const unit = totalWidth > 0 ? minimapWidth / totalWidth : 0
17
+ const s = -scrollX * unit
18
+ const w = Math.max(msaCanvasWidth * unit, 20)
14
19
  const polygonHeight = minimapHeight - MINIMAP_BAR_HEIGHT
15
- const polygonPoints = `${s + w},0 ${s},0 0,${polygonHeight} ${msaAreaWidth},${polygonHeight}`
20
+ const polygonPoints = `${s + w},0 ${s},0 0,${polygonHeight} ${minimapWidth},${polygonHeight}`
16
21
  return { unit, s, w, polygonHeight, polygonPoints }
17
22
  }