react-msaview 4.4.6 → 4.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 (122) hide show
  1. package/bundle/index.js +9 -9
  2. package/bundle/index.js.LICENSE.txt +8 -8
  3. package/bundle/index.js.map +1 -1
  4. package/dist/colorSchemes.d.ts +0 -6
  5. package/dist/colorSchemes.js +1 -119
  6. package/dist/colorSchemes.js.map +1 -1
  7. package/dist/components/ConservationTrack.d.ts +8 -0
  8. package/dist/components/ConservationTrack.js +54 -0
  9. package/dist/components/ConservationTrack.js.map +1 -0
  10. package/dist/components/Loading.js +14 -2
  11. package/dist/components/Loading.js.map +1 -1
  12. package/dist/components/MSAView.js +36 -0
  13. package/dist/components/MSAView.js.map +1 -1
  14. package/dist/components/SequenceTextArea.js +3 -2
  15. package/dist/components/SequenceTextArea.js.map +1 -1
  16. package/dist/components/TextTrack.d.ts +3 -3
  17. package/dist/components/TextTrack.js +4 -1
  18. package/dist/components/TextTrack.js.map +1 -1
  19. package/dist/components/Track.js +21 -8
  20. package/dist/components/Track.js.map +1 -1
  21. package/dist/components/dialogs/ExportSVGDialog.js +19 -3
  22. package/dist/components/dialogs/ExportSVGDialog.js.map +1 -1
  23. package/dist/components/header/GappynessSlider.d.ts +6 -0
  24. package/dist/components/header/GappynessSlider.js +19 -0
  25. package/dist/components/header/GappynessSlider.js.map +1 -0
  26. package/dist/components/header/Header.js +3 -1
  27. package/dist/components/header/Header.js.map +1 -1
  28. package/dist/components/header/HeaderMenu.js +30 -14
  29. package/dist/components/header/HeaderMenu.js.map +1 -1
  30. package/dist/components/minimap/MinimapSVG.js +4 -3
  31. package/dist/components/minimap/MinimapSVG.js.map +1 -1
  32. package/dist/components/msa/MSACanvasBlock.js +56 -42
  33. package/dist/components/msa/MSACanvasBlock.js.map +1 -1
  34. package/dist/components/msa/renderMSABlock.js +71 -26
  35. package/dist/components/msa/renderMSABlock.js.map +1 -1
  36. package/dist/components/msa/renderMSAMouseover.js +8 -1
  37. package/dist/components/msa/renderMSAMouseover.js.map +1 -1
  38. package/dist/components/tracks/renderTracksSvg.d.ts +29 -0
  39. package/dist/components/tracks/renderTracksSvg.js +83 -0
  40. package/dist/components/tracks/renderTracksSvg.js.map +1 -0
  41. package/dist/components/tree/TreeNodeMenu.js +2 -2
  42. package/dist/components/tree/TreeNodeMenu.js.map +1 -1
  43. package/dist/components/tree/renderTreeCanvas.d.ts +0 -1
  44. package/dist/components/tree/renderTreeCanvas.js +23 -24
  45. package/dist/components/tree/renderTreeCanvas.js.map +1 -1
  46. package/dist/constants.d.ts +22 -0
  47. package/dist/constants.js +26 -0
  48. package/dist/constants.js.map +1 -0
  49. package/dist/layout.js.map +1 -1
  50. package/dist/model/msaModel.js +3 -2
  51. package/dist/model/msaModel.js.map +1 -1
  52. package/dist/model/treeModel.js +9 -8
  53. package/dist/model/treeModel.js.map +1 -1
  54. package/dist/model.d.ts +271 -15
  55. package/dist/model.js +427 -128
  56. package/dist/model.js.map +1 -1
  57. package/dist/neighborJoining.d.ts +1 -0
  58. package/dist/neighborJoining.js +839 -0
  59. package/dist/neighborJoining.js.map +1 -0
  60. package/dist/neighborJoining.test.d.ts +1 -0
  61. package/dist/neighborJoining.test.js +110 -0
  62. package/dist/neighborJoining.test.js.map +1 -0
  63. package/dist/parsers/A3mMSA.d.ts +43 -0
  64. package/dist/parsers/A3mMSA.js +277 -0
  65. package/dist/parsers/A3mMSA.js.map +1 -0
  66. package/dist/parsers/A3mMSA.test.d.ts +1 -0
  67. package/dist/parsers/A3mMSA.test.js +138 -0
  68. package/dist/parsers/A3mMSA.test.js.map +1 -0
  69. package/dist/parsers/ClustalMSA.d.ts +4 -4
  70. package/dist/parsers/ClustalMSA.js +3 -1
  71. package/dist/parsers/ClustalMSA.js.map +1 -1
  72. package/dist/parsers/FastaMSA.js +17 -16
  73. package/dist/parsers/FastaMSA.js.map +1 -1
  74. package/dist/renderToSvg.d.ts +1 -0
  75. package/dist/renderToSvg.js +48 -18
  76. package/dist/renderToSvg.js.map +1 -1
  77. package/dist/rowCoordinateCalculations.js +2 -0
  78. package/dist/rowCoordinateCalculations.js.map +1 -1
  79. package/dist/types.d.ts +2 -3
  80. package/dist/util.js +17 -9
  81. package/dist/util.js.map +1 -1
  82. package/dist/version.d.ts +1 -1
  83. package/dist/version.js +1 -1
  84. package/package.json +6 -6
  85. package/src/colorSchemes.ts +1 -179
  86. package/src/components/ConservationTrack.tsx +104 -0
  87. package/src/components/Loading.tsx +44 -2
  88. package/src/components/MSAView.tsx +68 -0
  89. package/src/components/SequenceTextArea.tsx +3 -2
  90. package/src/components/TextTrack.tsx +7 -4
  91. package/src/components/Track.tsx +25 -9
  92. package/src/components/dialogs/ExportSVGDialog.tsx +25 -1
  93. package/src/components/header/GappynessSlider.tsx +35 -0
  94. package/src/components/header/Header.tsx +3 -1
  95. package/src/components/header/HeaderMenu.tsx +36 -15
  96. package/src/components/minimap/MinimapSVG.tsx +6 -3
  97. package/src/components/msa/MSACanvasBlock.tsx +66 -48
  98. package/src/components/msa/renderMSABlock.ts +103 -40
  99. package/src/components/msa/renderMSAMouseover.ts +9 -0
  100. package/src/components/tracks/renderTracksSvg.ts +157 -0
  101. package/src/components/tree/TreeNodeMenu.tsx +2 -2
  102. package/src/components/tree/renderTreeCanvas.ts +25 -34
  103. package/src/constants.ts +27 -0
  104. package/src/layout.ts +1 -6
  105. package/src/model/msaModel.ts +4 -2
  106. package/src/model/treeModel.ts +19 -8
  107. package/src/model.ts +517 -140
  108. package/src/neighborJoining.test.ts +129 -0
  109. package/src/neighborJoining.ts +885 -0
  110. package/src/parsers/A3mMSA.test.ts +164 -0
  111. package/src/parsers/A3mMSA.ts +321 -0
  112. package/src/parsers/ClustalMSA.ts +7 -5
  113. package/src/parsers/FastaMSA.ts +17 -17
  114. package/src/renderToSvg.tsx +105 -26
  115. package/src/rowCoordinateCalculations.ts +2 -0
  116. package/src/types.ts +2 -4
  117. package/src/util.ts +21 -8
  118. package/src/version.ts +1 -1
  119. package/dist/components/dialogs/TracklistDialog.d.ts +0 -7
  120. package/dist/components/dialogs/TracklistDialog.js +0 -23
  121. package/dist/components/dialogs/TracklistDialog.js.map +0 -1
  122. package/src/components/dialogs/TracklistDialog.tsx +0 -73
@@ -0,0 +1,104 @@
1
+ import React, { useEffect, useRef } from 'react'
2
+
3
+ import { observer } from 'mobx-react'
4
+
5
+ import type { MsaViewModel } from '../model'
6
+ import type { BasicTrack } from '../types'
7
+
8
+ const ConservationBlock = observer(function ({
9
+ model,
10
+ offsetX,
11
+ trackHeight,
12
+ }: {
13
+ model: MsaViewModel
14
+ offsetX: number
15
+ trackHeight: number
16
+ }) {
17
+ const { blockSize, scrollX, colWidth, highResScaleFactor, conservation } =
18
+ model
19
+
20
+ const ref = useRef<HTMLCanvasElement>(null)
21
+
22
+ useEffect(() => {
23
+ if (!ref.current) {
24
+ return
25
+ }
26
+
27
+ const ctx = ref.current.getContext('2d')
28
+ if (!ctx) {
29
+ return
30
+ }
31
+
32
+ ctx.resetTransform()
33
+ ctx.scale(highResScaleFactor, highResScaleFactor)
34
+ ctx.clearRect(0, 0, blockSize, trackHeight)
35
+ ctx.translate(-offsetX, 0)
36
+
37
+ const xStart = Math.max(0, Math.floor(offsetX / colWidth))
38
+ const xEnd = Math.max(0, Math.ceil((offsetX + blockSize) / colWidth))
39
+
40
+ for (let i = xStart; i < xEnd && i < conservation.length; i++) {
41
+ const value = conservation[i]!
42
+ const barHeight = value * trackHeight
43
+ const x = i * colWidth
44
+
45
+ const hue = value * 120
46
+ ctx.fillStyle = `hsl(${hue}, 70%, 50%)`
47
+ ctx.fillRect(x, trackHeight - barHeight, colWidth, barHeight)
48
+ }
49
+ }, [
50
+ blockSize,
51
+ colWidth,
52
+ trackHeight,
53
+ offsetX,
54
+ highResScaleFactor,
55
+ conservation,
56
+ ])
57
+
58
+ return (
59
+ <canvas
60
+ ref={ref}
61
+ height={trackHeight * highResScaleFactor}
62
+ width={blockSize * highResScaleFactor}
63
+ style={{
64
+ position: 'absolute',
65
+ left: scrollX + offsetX,
66
+ width: blockSize,
67
+ height: trackHeight,
68
+ }}
69
+ />
70
+ )
71
+ })
72
+
73
+ const ConservationTrack = observer(function ({
74
+ model,
75
+ track,
76
+ }: {
77
+ model: MsaViewModel
78
+ track: BasicTrack
79
+ }) {
80
+ const { blocksX, msaAreaWidth } = model
81
+ const trackHeight = track.model.height
82
+
83
+ return (
84
+ <div
85
+ style={{
86
+ position: 'relative',
87
+ height: trackHeight,
88
+ width: msaAreaWidth,
89
+ overflow: 'hidden',
90
+ }}
91
+ >
92
+ {blocksX.map(bx => (
93
+ <ConservationBlock
94
+ key={bx}
95
+ model={model}
96
+ offsetX={bx}
97
+ trackHeight={trackHeight}
98
+ />
99
+ ))}
100
+ </div>
101
+ )
102
+ })
103
+
104
+ export default ConservationTrack
@@ -10,6 +10,45 @@ import ImportForm from './import/ImportForm'
10
10
 
11
11
  import type { MsaViewModel } from '../model'
12
12
 
13
+ function LoadingSpinner() {
14
+ return (
15
+ <div
16
+ style={{ display: 'flex', alignItems: 'center', gap: 12, padding: 20 }}
17
+ >
18
+ <svg
19
+ width="24"
20
+ height="24"
21
+ viewBox="0 0 24 24"
22
+ xmlns="http://www.w3.org/2000/svg"
23
+ >
24
+ <style>
25
+ {`@keyframes spinner { to { transform: rotate(360deg); } }`}
26
+ </style>
27
+ <circle
28
+ cx="12"
29
+ cy="12"
30
+ r="10"
31
+ stroke="#ccc"
32
+ strokeWidth="3"
33
+ fill="none"
34
+ />
35
+ <path
36
+ d="M12 2a10 10 0 0 1 10 10"
37
+ stroke="#1976d2"
38
+ strokeWidth="3"
39
+ fill="none"
40
+ strokeLinecap="round"
41
+ style={{
42
+ animation: 'spinner 1s linear infinite',
43
+ transformOrigin: 'center',
44
+ }}
45
+ />
46
+ </svg>
47
+ <Typography variant="h6">Loading...</Typography>
48
+ </div>
49
+ )
50
+ }
51
+
13
52
  const Reset = observer(function ({
14
53
  model,
15
54
  error,
@@ -34,7 +73,8 @@ const Reset = observer(function ({
34
73
  })
35
74
 
36
75
  const Loading = observer(function ({ model }: { model: MsaViewModel }) {
37
- const { isLoading, dataInitialized } = model
76
+ const { isLoading, dataInitialized, msaFilehandle, treeFilehandle } = model
77
+ const hasPendingFilehandle = !!(msaFilehandle || treeFilehandle)
38
78
 
39
79
  return (
40
80
  <div>
@@ -43,10 +83,12 @@ const Loading = observer(function ({ model }: { model: MsaViewModel }) {
43
83
  >
44
84
  {dataInitialized ? (
45
85
  isLoading ? (
46
- <Typography variant="h4">Loading...</Typography>
86
+ <LoadingSpinner />
47
87
  ) : (
48
88
  <MSAView model={model} />
49
89
  )
90
+ ) : hasPendingFilehandle || isLoading ? (
91
+ <LoadingSpinner />
50
92
  ) : (
51
93
  <ImportForm model={model} />
52
94
  )}
@@ -3,6 +3,7 @@ import React, { Suspense } from 'react'
3
3
  import { observer } from 'mobx-react'
4
4
 
5
5
  import { HorizontalResizeHandle, VerticalResizeHandle } from './ResizeHandles'
6
+ import Track from './Track'
6
7
  import VerticalScrollbar from './VerticalScrollbar'
7
8
  import Header from './header/Header'
8
9
  import Minimap from './minimap/Minimap'
@@ -22,6 +23,72 @@ const TopArea = observer(function ({ model }: { model: MsaViewModel }) {
22
23
  )
23
24
  })
24
25
 
26
+ const TrackColumnIndicator = observer(function ({
27
+ model,
28
+ }: {
29
+ model: MsaViewModel
30
+ }) {
31
+ const {
32
+ mouseCol,
33
+ mouseClickCol,
34
+ colWidth,
35
+ scrollX,
36
+ treeAreaWidth,
37
+ resizeHandleWidth,
38
+ totalTrackAreaHeight,
39
+ } = model
40
+
41
+ const left = treeAreaWidth + resizeHandleWidth
42
+
43
+ return (
44
+ <>
45
+ {mouseCol !== undefined ? (
46
+ <div
47
+ style={{
48
+ position: 'absolute',
49
+ left: left + mouseCol * colWidth + scrollX,
50
+ top: 0,
51
+ width: colWidth,
52
+ height: totalTrackAreaHeight,
53
+ backgroundColor: 'rgba(0,0,0,0.15)',
54
+ pointerEvents: 'none',
55
+ zIndex: 100,
56
+ }}
57
+ />
58
+ ) : null}
59
+ {mouseClickCol !== undefined ? (
60
+ <div
61
+ style={{
62
+ position: 'absolute',
63
+ left: left + mouseClickCol * colWidth + scrollX,
64
+ top: 0,
65
+ width: colWidth,
66
+ height: totalTrackAreaHeight,
67
+ backgroundColor: 'rgba(128,128,0,0.2)',
68
+ pointerEvents: 'none',
69
+ zIndex: 100,
70
+ }}
71
+ />
72
+ ) : null}
73
+ </>
74
+ )
75
+ })
76
+
77
+ const TrackArea = observer(function ({ model }: { model: MsaViewModel }) {
78
+ const { turnedOnTracks } = model
79
+ if (turnedOnTracks.length === 0) {
80
+ return null
81
+ }
82
+ return (
83
+ <div style={{ position: 'relative' }}>
84
+ <TrackColumnIndicator model={model} />
85
+ {turnedOnTracks.map(track => (
86
+ <Track key={track.model.id} model={model} track={track} />
87
+ ))}
88
+ </div>
89
+ )
90
+ })
91
+
25
92
  const MainArea = observer(function ({ model }: { model: MsaViewModel }) {
26
93
  const { showVerticalScrollbar } = model
27
94
 
@@ -39,6 +106,7 @@ const View = observer(function ({ model }: { model: MsaViewModel }) {
39
106
  return (
40
107
  <div style={{ position: 'relative' }}>
41
108
  <TopArea model={model} />
109
+ <TrackArea model={model} />
42
110
  <MainArea model={model} />
43
111
  </div>
44
112
  )
@@ -24,10 +24,11 @@ export default function SequenceTextArea({ str }: { str: [string, string][] }) {
24
24
  const [showGaps, setShowGaps] = useState(false)
25
25
  const [showEmpty, setShowEmpty] = useState(false)
26
26
 
27
+ const removeGaps = (s: string) => s.replaceAll('-', '').replaceAll('.', '')
27
28
  const disp = str
28
- .map(([s1, s2]) => [s1, showGaps ? s2 : s2.replaceAll('-', '')] as const)
29
+ .map(([s1, s2]) => [s1, showGaps ? s2 : removeGaps(s2)] as const)
29
30
  .filter(f => (showEmpty ? true : !!f[1]))
30
- .map(([s1, s2]) => `>${s1}\n${showGaps ? s2 : s2.replaceAll('-', '')}`)
31
+ .map(([s1, s2]) => `>${s1}\n${showGaps ? s2 : removeGaps(s2)}`)
31
32
  .join('\n')
32
33
  return (
33
34
  <>
@@ -6,14 +6,14 @@ import { observer } from 'mobx-react'
6
6
  import { colorContrast } from '../util'
7
7
 
8
8
  import type { MsaViewModel } from '../model'
9
- import type { ITextTrack } from '../types'
9
+ import type { BasicTrack } from '../types'
10
10
 
11
11
  const AnnotationBlock = observer(function ({
12
12
  track,
13
13
  model,
14
14
  offsetX,
15
15
  }: {
16
- track: ITextTrack
16
+ track: BasicTrack
17
17
  model: MsaViewModel
18
18
  offsetX: number
19
19
  }) {
@@ -58,7 +58,7 @@ const AnnotationBlock = observer(function ({
58
58
 
59
59
  const xStart = Math.max(0, Math.floor(offsetX / colWidth))
60
60
  const xEnd = Math.max(0, Math.ceil((offsetX + blockSize) / colWidth))
61
- const str = data.slice(xStart, xEnd)
61
+ const str = data?.slice(xStart, xEnd)
62
62
  for (let i = 0; str && i < str.length; i++) {
63
63
  const letter = str[i]!
64
64
  const color = colorScheme[letter.toUpperCase()]
@@ -102,10 +102,13 @@ const AnnotationTrack = observer(function ({
102
102
  track,
103
103
  model,
104
104
  }: {
105
- track: ITextTrack
105
+ track: BasicTrack
106
106
  model: MsaViewModel
107
107
  }) {
108
108
  const { blocksX, msaAreaWidth, rowHeight } = model
109
+ if (!track.model.data) {
110
+ return null
111
+ }
109
112
  return (
110
113
  <div
111
114
  style={{
@@ -3,7 +3,6 @@ import React, { lazy, useEffect, useRef, useState } from 'react'
3
3
  import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown'
4
4
  import { IconButton, Menu, MenuItem } from '@mui/material'
5
5
  import { observer } from 'mobx-react'
6
- import normalizeWheel from 'normalize-wheel'
7
6
  import { makeStyles } from 'tss-react/mui'
8
7
 
9
8
  import type { MsaViewModel } from '../model'
@@ -28,8 +27,7 @@ export const TrackLabel = observer(function ({
28
27
  const [anchorEl, setAnchorEl] = useState<HTMLButtonElement>()
29
28
  const { drawLabels, rowHeight, treeAreaWidth: width } = model
30
29
  const {
31
- height,
32
- model: { name },
30
+ model: { name, height },
33
31
  } = track
34
32
  const { classes } = useStyles()
35
33
  const trackLabelHeight = Math.max(8, rowHeight - 8)
@@ -101,7 +99,7 @@ const Track = observer(function ({
101
99
 
102
100
  track: any
103
101
  }) {
104
- const { resizeHandleWidth } = model
102
+ const { resizeHandleWidth, colWidth, scrollX, numColumns } = model
105
103
  const {
106
104
  model: { height, error },
107
105
  } = track
@@ -113,9 +111,8 @@ const Track = observer(function ({
113
111
  if (!curr) {
114
112
  return
115
113
  }
116
- function onWheel(origEvent: WheelEvent) {
117
- const event = normalizeWheel(origEvent)
118
- deltaX.current += event.pixelX
114
+ function onWheel(event: WheelEvent) {
115
+ deltaX.current += event.deltaX
119
116
 
120
117
  if (!scheduled.current) {
121
118
  scheduled.current = true
@@ -125,18 +122,37 @@ const Track = observer(function ({
125
122
  scheduled.current = false
126
123
  })
127
124
  }
128
- origEvent.preventDefault()
125
+ event.preventDefault()
129
126
  }
130
127
  curr.addEventListener('wheel', onWheel)
131
128
  return () => {
132
129
  curr.removeEventListener('wheel', onWheel)
133
130
  }
134
131
  }, [model])
132
+
135
133
  return (
136
134
  <div key={track.id} style={{ display: 'flex', height }}>
137
135
  <TrackLabel model={model} track={track} />
138
136
  <div style={{ width: resizeHandleWidth, flexShrink: 0 }} />
139
- <div ref={ref}>
137
+ <div
138
+ ref={ref}
139
+ onMouseMove={event => {
140
+ if (!ref.current) {
141
+ return
142
+ }
143
+ const { left } = ref.current.getBoundingClientRect()
144
+ const mouseX = event.clientX - left - scrollX
145
+ const col = Math.floor(mouseX / colWidth)
146
+ if (col >= 0 && col < numColumns) {
147
+ model.setMousePos(col, undefined)
148
+ } else {
149
+ model.setMousePos(undefined, undefined)
150
+ }
151
+ }}
152
+ onMouseLeave={() => {
153
+ model.setMousePos(undefined, undefined)
154
+ }}
155
+ >
140
156
  {error ? (
141
157
  <div style={{ color: 'red', fontSize: 10 }}>{`${error}`}</div>
142
158
  ) : (
@@ -2,6 +2,7 @@ import React, { useState } from 'react'
2
2
 
3
3
  import { Dialog, ErrorMessage } from '@jbrowse/core/ui'
4
4
  import {
5
+ Alert,
5
6
  Button,
6
7
  DialogActions,
7
8
  DialogContent,
@@ -26,9 +27,16 @@ export default function ExportSVGDialog({
26
27
  onClose: () => void
27
28
  }) {
28
29
  const [includeMinimap, setIncludeMinimap] = useState(true)
30
+ const [includeTracks, setIncludeTracks] = useState(true)
29
31
  const [exportType, setExportType] = useState('viewport')
30
32
  const [error, setError] = useState<unknown>()
31
33
  const theme = useTheme()
34
+ const { totalWidth, totalHeight, treeAreaWidth, turnedOnTracks } = model
35
+ const hasTracks = turnedOnTracks.length > 0
36
+ const entireWidth = totalWidth + treeAreaWidth
37
+ const entireHeight = totalHeight
38
+ const isLargeExport =
39
+ exportType === 'entire' && (entireWidth > 10000 || entireHeight > 10000)
32
40
  return (
33
41
  <Dialog
34
42
  onClose={() => {
@@ -48,6 +56,15 @@ export default function ExportSVGDialog({
48
56
  setIncludeMinimap(!includeMinimap)
49
57
  }}
50
58
  />
59
+ {hasTracks ? (
60
+ <Checkbox2
61
+ label="Include tracks?"
62
+ checked={includeTracks}
63
+ onChange={() => {
64
+ setIncludeTracks(!includeTracks)
65
+ }}
66
+ />
67
+ ) : null}
51
68
  <div>
52
69
  <FormControl>
53
70
  <FormLabel>Export type</FormLabel>
@@ -70,6 +87,12 @@ export default function ExportSVGDialog({
70
87
  </RadioGroup>
71
88
  </FormControl>
72
89
  </div>
90
+ {isLargeExport ? (
91
+ <Alert severity="warning" style={{ marginTop: 8 }}>
92
+ The entire MSA is very large ({Math.round(entireWidth)}x
93
+ {Math.round(entireHeight)} pixels). Export may be slow or fail.
94
+ </Alert>
95
+ ) : null}
73
96
  </DialogContent>
74
97
  <DialogActions>
75
98
  <Button
@@ -83,13 +106,14 @@ export default function ExportSVGDialog({
83
106
  theme,
84
107
  includeMinimap:
85
108
  exportType === 'entire' ? false : includeMinimap,
109
+ includeTracks: hasTracks && includeTracks,
86
110
  exportType,
87
111
  })
112
+ onClose()
88
113
  } catch (e) {
89
114
  console.error(e)
90
115
  setError(e)
91
116
  }
92
- onClose()
93
117
  })()
94
118
  }}
95
119
  >
@@ -0,0 +1,35 @@
1
+ import React from 'react'
2
+
3
+ import { Slider, Typography } from '@mui/material'
4
+ import { observer } from 'mobx-react'
5
+
6
+ import type { MsaViewModel } from '../../model'
7
+
8
+ const GappynessSlider = observer(function GappynessSlider({
9
+ model,
10
+ }: {
11
+ model: MsaViewModel
12
+ }) {
13
+ const { hideGaps, allowedGappyness } = model
14
+ if (!hideGaps) {
15
+ return null
16
+ }
17
+ return (
18
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
19
+ <Typography style={{ whiteSpace: 'nowrap' }}>
20
+ Hide columns w/ &gt;{allowedGappyness}% gaps
21
+ </Typography>
22
+ <Slider
23
+ style={{ width: 100 }}
24
+ min={1}
25
+ max={100}
26
+ value={allowedGappyness}
27
+ onChange={(_, val) => {
28
+ model.setAllowedGappyness(val)
29
+ }}
30
+ />
31
+ </div>
32
+ )
33
+ })
34
+
35
+ export default GappynessSlider
@@ -5,6 +5,7 @@ import Help from '@mui/icons-material/Help'
5
5
  import { IconButton } from '@mui/material'
6
6
  import { observer } from 'mobx-react'
7
7
 
8
+ import GappynessSlider from './GappynessSlider'
8
9
  import HeaderInfoArea from './HeaderInfoArea'
9
10
  import HeaderMenu from './HeaderMenu'
10
11
  import HeaderStatusArea from './HeaderStatusArea'
@@ -30,7 +31,8 @@ const Header = observer(function ({ model }: { model: MsaViewModel }) {
30
31
  <ZoomControls model={model} />
31
32
  {model.showZoomStar ? <ZoomStar model={model} /> : null}
32
33
  <ZoomMenu model={model} />
33
- <div style={{ margin: 'auto' }}>
34
+ <GappynessSlider model={model} />
35
+ <div style={{ paddingLeft: 20, margin: 'auto' }}>
34
36
  <MultiAlignmentSelector model={model} />
35
37
  </div>
36
38
  <HeaderInfoArea model={model} />
@@ -1,10 +1,10 @@
1
1
  import React, { lazy } from 'react'
2
2
 
3
3
  import CascadingMenuButton from '@jbrowse/core/ui/CascadingMenuButton'
4
+ import AccountTree from '@mui/icons-material/AccountTree'
4
5
  import Assignment from '@mui/icons-material/Assignment'
5
6
  import FilterAlt from '@mui/icons-material/FilterAlt'
6
7
  import FolderOpen from '@mui/icons-material/FolderOpen'
7
- import List from '@mui/icons-material/List'
8
8
  import MoreVert from '@mui/icons-material/Menu'
9
9
  import PhotoCamera from '@mui/icons-material/PhotoCamera'
10
10
  import Search from '@mui/icons-material/Search'
@@ -16,7 +16,6 @@ import type { MsaViewModel } from '../../model'
16
16
 
17
17
  // lazies
18
18
  const MetadataDialog = lazy(() => import('../dialogs/MetadataDialog'))
19
- const TracklistDialog = lazy(() => import('../dialogs/TracklistDialog'))
20
19
  const ExportSVGDialog = lazy(() => import('../dialogs/ExportSVGDialog'))
21
20
  const FeatureFilterDialog = lazy(() => import('../dialogs/FeatureDialog'))
22
21
  const SettingsDialog = lazy(() => import('../dialogs/SettingsDialog'))
@@ -26,7 +25,14 @@ const UserProvidedDomainsDialog = lazy(
26
25
  const InterProScanDialog = lazy(() => import('../dialogs/InterProScanDialog'))
27
26
 
28
27
  const HeaderMenu = observer(({ model }: { model: MsaViewModel }) => {
29
- const { showDomains, actuallyShowDomains, subFeatureRows, noDomains } = model
28
+ const {
29
+ showDomains,
30
+ actuallyShowDomains,
31
+ subFeatureRows,
32
+ noDomains,
33
+ tracks,
34
+ turnedOffTracks,
35
+ } = model
30
36
  return (
31
37
  <CascadingMenuButton
32
38
  menuItems={[
@@ -63,19 +69,18 @@ const HeaderMenu = observer(({ model }: { model: MsaViewModel }) => {
63
69
  },
64
70
  },
65
71
  {
66
- label: 'Extra tracks',
67
- icon: List,
68
- onClick: () => {
69
- model.queueDialog(onClose => [
70
- TracklistDialog,
71
- {
72
- model,
73
- onClose,
74
- },
75
- ])
76
- },
72
+ label: 'Tracks',
73
+ icon: Visibility,
74
+ type: 'subMenu',
75
+ subMenu: tracks.map(track => ({
76
+ label: track.model.name,
77
+ type: 'checkbox' as const,
78
+ checked: !turnedOffTracks.has(track.model.id),
79
+ onClick: () => {
80
+ model.toggleTrack(track.model.id)
81
+ },
82
+ })),
77
83
  },
78
-
79
84
  {
80
85
  label: 'Export SVG',
81
86
  icon: PhotoCamera,
@@ -89,6 +94,22 @@ const HeaderMenu = observer(({ model }: { model: MsaViewModel }) => {
89
94
  ])
90
95
  },
91
96
  },
97
+ ...(model.rows.length >= 2
98
+ ? [
99
+ {
100
+ label: 'Calculate neighbor joining tree (BLOSUM62)',
101
+ icon: AccountTree,
102
+ onClick: () => {
103
+ try {
104
+ model.calculateNeighborJoiningTreeFromMSA()
105
+ } catch (e) {
106
+ console.error('Failed to calculate NJ tree:', e)
107
+ model.setError(e)
108
+ }
109
+ },
110
+ },
111
+ ]
112
+ : []),
92
113
  {
93
114
  label: 'Features/protein domains',
94
115
  type: 'subMenu',
@@ -21,7 +21,8 @@ const MinimapSVG = observer(({ model }: { model: MsaViewModel }) => {
21
21
  const right = left + W
22
22
  const s = left * unit
23
23
  const e = right * unit
24
- const fill = 'rgba(66, 119, 127, 0.3)'
24
+ const fillColor = 'rgb(66, 119, 127)'
25
+ const fillOpacity = 0.3
25
26
 
26
27
  return (
27
28
  <>
@@ -38,12 +39,14 @@ const MinimapSVG = observer(({ model }: { model: MsaViewModel }) => {
38
39
  y={0}
39
40
  width={e - s}
40
41
  height={BAR_HEIGHT}
41
- fill={fill}
42
+ fill={fillColor}
43
+ fillOpacity={fillOpacity}
42
44
  stroke="#555"
43
45
  />
44
46
  <g transform={`translate(0 ${BAR_HEIGHT})`}>
45
47
  <polygon
46
- fill={fill}
48
+ fill={fillColor}
49
+ fillOpacity={fillOpacity}
47
50
  points={[
48
51
  [e, 0],
49
52
  [s, 0],