react-msaview 6.0.0 → 6.1.1

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 +98 -98
  2. package/bundle/index.js.map +4 -4
  3. package/dist/columnCounts.d.ts +7 -0
  4. package/dist/columnCounts.js +18 -0
  5. package/dist/columnCounts.js.map +1 -1
  6. package/dist/components/DragHandle.d.ts +25 -0
  7. package/dist/components/DragHandle.js +49 -0
  8. package/dist/components/DragHandle.js.map +1 -0
  9. package/dist/components/ResizeHandles.d.ts +4 -2
  10. package/dist/components/ResizeHandles.js +29 -32
  11. package/dist/components/ResizeHandles.js.map +1 -1
  12. package/dist/components/VerticalScrollbar.js +2 -2
  13. package/dist/components/VerticalScrollbar.js.map +1 -1
  14. package/dist/components/dialogs/InterProScanDialog.js +52 -129
  15. package/dist/components/dialogs/InterProScanDialog.js.map +1 -1
  16. package/dist/components/header/settingsMenuItems.js +16 -1
  17. package/dist/components/header/settingsMenuItems.js.map +1 -1
  18. package/dist/components/minimap/Minimap.js +32 -4
  19. package/dist/components/minimap/Minimap.js.map +1 -1
  20. package/dist/components/msa/MSACanvas.js +8 -2
  21. package/dist/components/msa/MSACanvas.js.map +1 -1
  22. package/dist/components/msa/MSACanvasBlock.js +12 -3
  23. package/dist/components/msa/MSACanvasBlock.js.map +1 -1
  24. package/dist/components/msa/msaRaster.d.ts +63 -0
  25. package/dist/components/msa/msaRaster.js +252 -0
  26. package/dist/components/msa/msaRaster.js.map +1 -0
  27. package/dist/components/msa/renderMSABlock.d.ts +2 -1
  28. package/dist/components/msa/renderMSABlock.js +13 -21
  29. package/dist/components/msa/renderMSABlock.js.map +1 -1
  30. package/dist/components/msa/tileColor.d.ts +6 -0
  31. package/dist/components/msa/tileColor.js +35 -0
  32. package/dist/components/msa/tileColor.js.map +1 -0
  33. package/dist/components/renderCtx.d.ts +1 -1
  34. package/dist/components/tracks/TrackBlocks.d.ts +14 -0
  35. package/dist/components/tracks/TrackBlocks.js +65 -0
  36. package/dist/components/tracks/TrackBlocks.js.map +1 -0
  37. package/dist/components/tracks/{renderTracksSvg.d.ts → drawTracks.d.ts} +22 -10
  38. package/dist/components/tracks/drawTracks.js +193 -0
  39. package/dist/components/tracks/drawTracks.js.map +1 -0
  40. package/dist/components/tree/TreeCanvas.js +8 -2
  41. package/dist/components/tree/TreeCanvas.js.map +1 -1
  42. package/dist/components/tree/TreeCanvasBlock.js +7 -4
  43. package/dist/components/tree/TreeCanvasBlock.js.map +1 -1
  44. package/dist/components/tree/renderTreeCanvas.js +27 -21
  45. package/dist/components/tree/renderTreeCanvas.js.map +1 -1
  46. package/dist/constants.d.ts +3 -0
  47. package/dist/constants.js +8 -0
  48. package/dist/constants.js.map +1 -1
  49. package/dist/model/DataModel.d.ts +1 -4
  50. package/dist/model/DataModel.js +8 -10
  51. package/dist/model/DataModel.js.map +1 -1
  52. package/dist/model/treeModel.d.ts +1 -1
  53. package/dist/model/treeModel.js +1 -1
  54. package/dist/model.d.ts +36 -7
  55. package/dist/model.js +113 -58
  56. package/dist/model.js.map +1 -1
  57. package/dist/neighborJoining.js +34 -18
  58. package/dist/neighborJoining.js.map +1 -1
  59. package/dist/renderTestEnv.d.ts +43 -0
  60. package/dist/renderTestEnv.js +88 -0
  61. package/dist/renderTestEnv.js.map +1 -0
  62. package/dist/renderToSvg.js +1 -1
  63. package/dist/renderToSvg.js.map +1 -1
  64. package/dist/sequenceLogo.d.ts +28 -0
  65. package/dist/sequenceLogo.js +47 -0
  66. package/dist/sequenceLogo.js.map +1 -0
  67. package/dist/types.d.ts +2 -0
  68. package/dist/version.d.ts +1 -1
  69. package/dist/version.js +1 -1
  70. package/package.json +3 -3
  71. package/src/columnCounts.test.ts +15 -0
  72. package/src/columnCounts.ts +19 -0
  73. package/src/components/DragHandle.tsx +81 -0
  74. package/src/components/ResizeHandles.tsx +46 -54
  75. package/src/components/VerticalScrollbar.tsx +3 -2
  76. package/src/components/dialogs/InterProScanDialog.tsx +54 -131
  77. package/src/components/header/settingsMenuItems.ts +19 -1
  78. package/src/components/minimap/Minimap.tsx +47 -4
  79. package/src/components/msa/MSACanvas.tsx +30 -10
  80. package/src/components/msa/MSACanvasBlock.tsx +13 -3
  81. package/src/components/msa/msaRaster.test.ts +145 -0
  82. package/src/components/msa/msaRaster.ts +350 -0
  83. package/src/components/msa/renderMSABlock.ts +15 -20
  84. package/src/components/msa/tileColor.ts +37 -0
  85. package/src/components/renderCtx.ts +2 -0
  86. package/src/components/tracks/TrackBlocks.tsx +111 -0
  87. package/src/components/tracks/drawTracks.ts +323 -0
  88. package/src/components/tree/TreeCanvas.tsx +16 -4
  89. package/src/components/tree/TreeCanvasBlock.tsx +18 -11
  90. package/src/components/tree/renderTreeCanvas.test.ts +55 -0
  91. package/src/components/tree/renderTreeCanvas.ts +28 -19
  92. package/src/constants.ts +10 -0
  93. package/src/impgRender.test.tsx +2 -57
  94. package/src/largeTree.test.ts +16 -2
  95. package/src/model/DataModel.ts +11 -11
  96. package/src/model/treeModel.ts +1 -1
  97. package/src/model.ts +128 -60
  98. package/src/neighborJoining.ts +36 -18
  99. package/src/renderDomainsSvg.test.tsx +2 -57
  100. package/src/renderMinimapSvg.test.tsx +2 -56
  101. package/src/renderSequenceLogoSvg.test.tsx +176 -0
  102. package/src/renderTestEnv.ts +96 -0
  103. package/src/renderToSvg.tsx +1 -1
  104. package/src/sequenceLogo.test.ts +88 -0
  105. package/src/sequenceLogo.ts +63 -0
  106. package/src/types.ts +7 -1
  107. package/src/version.ts +1 -1
  108. package/dist/components/ConservationTrack.d.ts +0 -8
  109. package/dist/components/ConservationTrack.js +0 -49
  110. package/dist/components/ConservationTrack.js.map +0 -1
  111. package/dist/components/ScrollbarThumb.d.ts +0 -14
  112. package/dist/components/ScrollbarThumb.js +0 -28
  113. package/dist/components/ScrollbarThumb.js.map +0 -1
  114. package/dist/components/TextTrack.d.ts +0 -8
  115. package/dist/components/TextTrack.js +0 -58
  116. package/dist/components/TextTrack.js.map +0 -1
  117. package/dist/components/tracks/renderTracksSvg.js +0 -126
  118. package/dist/components/tracks/renderTracksSvg.js.map +0 -1
  119. package/src/components/ConservationTrack.tsx +0 -99
  120. package/src/components/ScrollbarThumb.tsx +0 -48
  121. package/src/components/TextTrack.tsx +0 -103
  122. package/src/components/tracks/renderTracksSvg.ts +0 -247
@@ -0,0 +1,47 @@
1
+ import { letterOfResidueSlot } from './columnCounts.js';
2
+ /** the y-axis ceiling of a logo: the information content of a fully conserved
3
+ * column, log2 of the alphabet size */
4
+ export function maxBitsFor(sequenceType) {
5
+ return Math.log2(sequenceType === 'amino' ? 20 : 4);
6
+ }
7
+ /**
8
+ * One column of a sequence logo, tallest letter last so a caller can draw the
9
+ * stack bottom-up in array order.
10
+ *
11
+ * Heights follow Schneider & Stephens: the column's information content is
12
+ * `log2(alphabet) - H` bits, and each residue takes the share of that its own
13
+ * frequency earns. Frequencies come from the non-gap residues only, so a column
14
+ * of two residues and eight gaps is read as a two-way choice rather than a
15
+ * ten-way one -- otherwise every gappy column would look uninformative for the
16
+ * wrong reason.
17
+ *
18
+ * The whole stack is then scaled by the non-gap fraction, which plain WebLogo
19
+ * does not do. Here it earns its place: the track sits directly under an
20
+ * alignment where the gaps are visible, and a column that is 90% gap drawing a
21
+ * full-height stack off its one remaining pair of residues reads as a conserved
22
+ * site. Scaling makes stack height mean "how much this column tells you about
23
+ * the family", matching what the conservation track above it already plots.
24
+ */
25
+ export function columnLogoStack(colStats, col, maxBits) {
26
+ const total = colStats.total(col);
27
+ const nonGapTotal = total - colStats.gapCount(col);
28
+ if (nonGapTotal === 0) {
29
+ return [];
30
+ }
31
+ const informationContent = Math.max(0, maxBits - colStats.entropy(col)) * (nonGapTotal / total);
32
+ if (informationContent === 0) {
33
+ return [];
34
+ }
35
+ const stack = [];
36
+ colStats.forEachResidue(col, (slot, count) => {
37
+ stack.push({
38
+ letter: letterOfResidueSlot(slot),
39
+ bits: (count / nonGapTotal) * informationContent,
40
+ });
41
+ });
42
+ // ascending, so drawing in order stacks the tallest letter on top. Ties break
43
+ // on letter to keep the stack order stable between redraws.
44
+ stack.sort((a, b) => a.bits - b.bits || a.letter.localeCompare(b.letter));
45
+ return stack;
46
+ }
47
+ //# sourceMappingURL=sequenceLogo.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sequenceLogo.js","sourceRoot":"","sources":["../src/sequenceLogo.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAA;AAUvD;uCACuC;AACvC,MAAM,UAAU,UAAU,CAAC,YAAqC;IAC9D,OAAO,IAAI,CAAC,IAAI,CAAC,YAAY,KAAK,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;AACrD,CAAC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,eAAe,CAC7B,QAAsB,EACtB,GAAW,EACX,OAAe;IAEf,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;IACjC,MAAM,WAAW,GAAG,KAAK,GAAG,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAA;IAClD,IAAI,WAAW,KAAK,CAAC,EAAE,CAAC;QACtB,OAAO,EAAE,CAAA;IACX,CAAC;IAED,MAAM,kBAAkB,GACtB,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,WAAW,GAAG,KAAK,CAAC,CAAA;IACtE,IAAI,kBAAkB,KAAK,CAAC,EAAE,CAAC;QAC7B,OAAO,EAAE,CAAA;IACX,CAAC;IAED,MAAM,KAAK,GAAiB,EAAE,CAAA;IAC9B,QAAQ,CAAC,cAAc,CAAC,GAAG,EAAE,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE;QAC3C,KAAK,CAAC,IAAI,CAAC;YACT,MAAM,EAAE,mBAAmB,CAAC,IAAI,CAAC;YACjC,IAAI,EAAE,CAAC,KAAK,GAAG,WAAW,CAAC,GAAG,kBAAkB;SACjD,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;IACF,8EAA8E;IAC9E,4DAA4D;IAC5D,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAA;IACzE,OAAO,KAAK,CAAA;AACd,CAAC"}
package/dist/types.d.ts CHANGED
@@ -4,11 +4,13 @@ export interface Accession {
4
4
  name: string;
5
5
  description: string;
6
6
  }
7
+ export type TrackKind = 'text' | 'bar' | 'logo';
7
8
  export interface BasicTrackModel {
8
9
  id: string;
9
10
  name: string;
10
11
  associatedRowName?: string;
11
12
  height: number;
13
+ kind: TrackKind;
12
14
  }
13
15
  export interface TextTrackModel extends BasicTrackModel {
14
16
  customColorScheme?: Record<string, string>;
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const version = "6.0.0";
1
+ export declare const version = "6.1.1";
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
- export const version = '6.0.0';
1
+ export const version = '6.1.1';
2
2
  //# sourceMappingURL=version.js.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "react-msaview",
3
3
  "author": "Colin",
4
- "version": "6.0.0",
4
+ "version": "6.1.1",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "repository": {
@@ -43,8 +43,8 @@
43
43
  "@gmod/newick": "^1.0.0",
44
44
  "colord": "^2.9.3",
45
45
  "flatbush": "^4.6.2",
46
- "@jbrowse/svgcanvas": "6.0.0",
47
- "msa-parsers": "6.0.0"
46
+ "@jbrowse/svgcanvas": "6.1.1",
47
+ "msa-parsers": "6.1.1"
48
48
  },
49
49
  "scripts": {
50
50
  "clean": "rimraf dist bundle",
@@ -47,6 +47,21 @@ describe('columnCounts', () => {
47
47
  expect(counts.total(3)).toBe(1)
48
48
  })
49
49
 
50
+ test('entropy is taken over the non-gap residues only', () => {
51
+ const counts = columnCountsFromColumns(['AAAA', 'AACC', 'ACGT', 'AC--'])
52
+ // fully conserved: no choice left to make
53
+ expect(counts.entropy(0)).toBeCloseTo(0, 10)
54
+ // even two-way and four-way splits
55
+ expect(counts.entropy(1)).toBeCloseTo(1, 10)
56
+ expect(counts.entropy(2)).toBeCloseTo(2, 10)
57
+ // the two gaps do not turn a 1-bit choice into a 2-bit one
58
+ expect(counts.entropy(3)).toBeCloseTo(1, 10)
59
+ })
60
+
61
+ test('an all-gap column has no entropy rather than NaN', () => {
62
+ expect(columnCountsFromColumns(['----']).entropy(0)).toBe(0)
63
+ })
64
+
50
65
  test('residue slots round-trip to their letters', () => {
51
66
  const counts = columnCountsFromColumns(['WY'])
52
67
  const slots: number[] = []
@@ -109,6 +109,25 @@ export class ColumnCounts {
109
109
  }
110
110
  }
111
111
 
112
+ /**
113
+ * Shannon entropy of a column in bits, taken over its non-gap residues only:
114
+ * a column of two residues and eight gaps is a two-way choice, not a ten-way
115
+ * one, and gap occupancy is accounted for separately by every caller. 0 for
116
+ * an all-gap column.
117
+ */
118
+ entropy(col: number) {
119
+ const nonGapTotal = this.total(col) - this.gapCount(col)
120
+ if (nonGapTotal === 0) {
121
+ return 0
122
+ }
123
+ let entropy = 0
124
+ this.forEachResidue(col, (_slot, count) => {
125
+ const freq = count / nonGapTotal
126
+ entropy -= freq * Math.log2(freq)
127
+ })
128
+ return entropy
129
+ }
130
+
112
131
  /** non-gap [letter, count] pairs for one column, for display */
113
132
  residueEntries(col: number) {
114
133
  const entries: [string, number][] = []
@@ -0,0 +1,81 @@
1
+ import React, { useState } from 'react'
2
+
3
+ import { useDragScroll } from '../useDragScroll.ts'
4
+
5
+ export const scrollbarThumbFill = 'rgba(66,119,127,0.3)'
6
+
7
+ // The scroll thumbs (minimap, vertical scrollbar) and the resize dividers are
8
+ // one widget -- press, drag along an axis, report the pixel delta -- differing
9
+ // only in the affordance they present, so the variant carries the whole
10
+ // difference rather than a second copy of the drag wiring.
11
+ const variants: Record<
12
+ 'thumb' | 'resizer',
13
+ {
14
+ fill: string
15
+ fillHovered: string
16
+ cursor: (axis: 'x' | 'y') => string
17
+ position: React.CSSProperties['position']
18
+ zIndex?: number
19
+ }
20
+ > = {
21
+ thumb: {
22
+ fill: scrollbarThumbFill,
23
+ fillHovered: 'rgba(66,119,127,0.6)',
24
+ cursor: () => 'pointer',
25
+ position: 'absolute',
26
+ zIndex: 100,
27
+ },
28
+ resizer: {
29
+ fill: 'rgba(200,200,200)',
30
+ fillHovered: 'rgba(150,150,150)',
31
+ cursor: axis => (axis === 'x' ? 'ew-resize' : 'ns-resize'),
32
+ position: 'relative',
33
+ },
34
+ }
35
+
36
+ /**
37
+ * A handle dragged along a single axis. `getStart` is read at mousedown and
38
+ * handed back to `onDrag` alongside the pixel delta since, so a caller only
39
+ * supplies the dimension it drives and where the handle sits.
40
+ *
41
+ * Memoize `onDrag` (useCallback) so the document listeners are not re-attached
42
+ * on every render.
43
+ */
44
+ export default function DragHandle({
45
+ axis,
46
+ variant,
47
+ getStart,
48
+ onDrag,
49
+ style,
50
+ }: {
51
+ axis: 'x' | 'y'
52
+ variant: keyof typeof variants
53
+ getStart: () => number
54
+ onDrag: (delta: number, startValue: number) => void
55
+ style: React.CSSProperties
56
+ }) {
57
+ const [hovered, setHovered] = useState(false)
58
+ const { startDrag } = useDragScroll(axis, onDrag)
59
+ const { fill, fillHovered, cursor, position, zIndex } = variants[variant]
60
+
61
+ return (
62
+ <div
63
+ onMouseEnter={() => {
64
+ setHovered(true)
65
+ }}
66
+ onMouseLeave={() => {
67
+ setHovered(false)
68
+ }}
69
+ onMouseDown={event => {
70
+ startDrag(event, getStart())
71
+ }}
72
+ style={{
73
+ position,
74
+ zIndex,
75
+ cursor: cursor(axis),
76
+ background: hovered ? fillHovered : fill,
77
+ ...style,
78
+ }}
79
+ />
80
+ )
81
+ }
@@ -1,53 +1,11 @@
1
- import React, { useCallback, useState } from 'react'
1
+ import React, { useCallback } from 'react'
2
2
 
3
3
  import { observer } from 'mobx-react'
4
4
 
5
- import { useDragScroll } from '../useDragScroll.ts'
5
+ import DragHandle from './DragHandle.tsx'
6
6
 
7
7
  import type { MsaViewModel } from '../model.ts'
8
-
9
- const handleBg = 'rgba(200,200,200)'
10
- const handleBgHover = 'rgba(150,150,150)'
11
-
12
- /**
13
- * A draggable divider. `getStart` is read at mousedown and handed back to
14
- * `onDrag` alongside the pixel delta since, so each handle below only has to say
15
- * which model dimension it resizes and where it sits.
16
- */
17
- function ResizeHandle({
18
- axis,
19
- getStart,
20
- onDrag,
21
- style,
22
- }: {
23
- axis: 'x' | 'y'
24
- getStart: () => number
25
- onDrag: (delta: number, startValue: number) => void
26
- style: React.CSSProperties
27
- }) {
28
- const [hovered, setHovered] = useState(false)
29
- const { startDrag } = useDragScroll(axis, onDrag)
30
-
31
- return (
32
- <div
33
- onMouseDown={event => {
34
- startDrag(event, getStart())
35
- }}
36
- onMouseEnter={() => {
37
- setHovered(true)
38
- }}
39
- onMouseLeave={() => {
40
- setHovered(false)
41
- }}
42
- style={{
43
- position: 'relative',
44
- cursor: axis === 'x' ? 'ew-resize' : 'ns-resize',
45
- background: hovered ? handleBgHover : handleBg,
46
- ...style,
47
- }}
48
- />
49
- )
50
- }
8
+ import type { TrackKind } from '../types.ts'
51
9
 
52
10
  export const VerticalResizeHandle = observer(function ({
53
11
  model,
@@ -61,8 +19,9 @@ export const VerticalResizeHandle = observer(function ({
61
19
  [model],
62
20
  )
63
21
  return (
64
- <ResizeHandle
22
+ <DragHandle
65
23
  axis="x"
24
+ variant="resizer"
66
25
  getStart={() => model.treeAreaWidth}
67
26
  onDrag={onDrag}
68
27
  style={{ width: model.resizeHandleWidth }}
@@ -82,8 +41,9 @@ export const HorizontalResizeHandle = observer(function ({
82
41
  [model],
83
42
  )
84
43
  return (
85
- <ResizeHandle
44
+ <DragHandle
86
45
  axis="y"
46
+ variant="resizer"
87
47
  getStart={() => model.height}
88
48
  onDrag={onDrag}
89
49
  style={{ width: '100%', height: model.resizeHandleWidth }}
@@ -91,21 +51,53 @@ export const HorizontalResizeHandle = observer(function ({
91
51
  )
92
52
  })
93
53
 
94
- export const ConservationTrackResizeHandle = observer(function ({
54
+ // The height behind each resizable track kind. Both bar tracks read one height
55
+ // so conservation and property conservation stay directly comparable; the logo
56
+ // keeps its own, being taller by default. Text tracks are absent: they are one
57
+ // alignment row tall and follow rowHeight, so the zoom controls already size
58
+ // them.
59
+ const trackHeights: Partial<
60
+ Record<
61
+ TrackKind,
62
+ {
63
+ get: (model: MsaViewModel) => number
64
+ set: (model: MsaViewModel, height: number) => void
65
+ }
66
+ >
67
+ > = {
68
+ bar: {
69
+ get: model => model.conservationTrackHeight,
70
+ set: (model, height) => {
71
+ model.setConservationTrackHeight(height)
72
+ },
73
+ },
74
+ logo: {
75
+ get: model => model.sequenceLogoTrackHeight,
76
+ set: (model, height) => {
77
+ model.setSequenceLogoTrackHeight(height)
78
+ },
79
+ },
80
+ }
81
+
82
+ export const TrackResizeHandle = observer(function ({
95
83
  model,
84
+ kind,
96
85
  }: {
97
86
  model: MsaViewModel
87
+ kind: TrackKind
98
88
  }) {
89
+ const height = trackHeights[kind]
99
90
  const onDrag = useCallback(
100
91
  (delta: number, startHeight: number) => {
101
- model.setConservationTrackHeight(Math.max(10, startHeight + delta))
92
+ height?.set(model, Math.max(10, startHeight + delta))
102
93
  },
103
- [model],
94
+ [model, height],
104
95
  )
105
- return (
106
- <ResizeHandle
96
+ return height ? (
97
+ <DragHandle
107
98
  axis="y"
108
- getStart={() => model.conservationTrackHeight}
99
+ variant="resizer"
100
+ getStart={() => height.get(model)}
109
101
  onDrag={onDrag}
110
102
  style={{
111
103
  position: 'absolute',
@@ -116,5 +108,5 @@ export const ConservationTrackResizeHandle = observer(function ({
116
108
  zIndex: 1,
117
109
  }}
118
110
  />
119
- )
111
+ ) : null
120
112
  })
@@ -2,7 +2,7 @@ import React, { useCallback } from 'react'
2
2
 
3
3
  import { observer } from 'mobx-react'
4
4
 
5
- import ScrollbarThumb from './ScrollbarThumb.tsx'
5
+ import DragHandle from './DragHandle.tsx'
6
6
 
7
7
  import type { MsaViewModel } from '../model.ts'
8
8
 
@@ -32,8 +32,9 @@ const VerticalScrollbar = observer(({ model }: { model: MsaViewModel }) => {
32
32
  boxSizing: 'border-box',
33
33
  }}
34
34
  >
35
- <ScrollbarThumb
35
+ <DragHandle
36
36
  axis="y"
37
+ variant="thumb"
37
38
  getStart={() => model.scrollY}
38
39
  onDrag={onDrag}
39
40
  style={{
@@ -10,6 +10,57 @@ import { launchInterProScan } from '../../launchInterProScan.ts'
10
10
 
11
11
  import type { MsaViewModel } from '../../model.ts'
12
12
 
13
+ const FAMILIES = 'Families, domains, sites & repeats'
14
+ const FEATURES = 'Other sequence features'
15
+ const STRUCTURAL = 'Structural domains'
16
+ const OTHER = 'Other category'
17
+
18
+ // The analyses the EBI iprscan5 endpoint accepts, all on by default. Static, so
19
+ // it lives out here rather than being rebuilt on every render of the dialog.
20
+ const interProScanPrograms = [
21
+ {
22
+ name: 'NCBIfam',
23
+ category: FAMILIES,
24
+ description: 'NCBI RefSeq FAMs including TIGRFAMs',
25
+ },
26
+ {
27
+ name: 'SFLD',
28
+ category: FAMILIES,
29
+ description: 'Structure function linkage database',
30
+ },
31
+ {
32
+ name: 'Phobius',
33
+ category: FEATURES,
34
+ description:
35
+ 'A combined transmembrane topology and signal peptide predictor',
36
+ },
37
+ { name: 'SignalP', category: FEATURES },
38
+ { name: 'SignalP_EUK', category: OTHER },
39
+ { name: 'SignalP_GRAM_POSITIVE', category: OTHER },
40
+ { name: 'SignalP_GRAM_NEGATIVE', category: OTHER },
41
+ { name: 'SuperFamily', category: STRUCTURAL },
42
+ { name: 'Panther', category: FAMILIES },
43
+ { name: 'Gene3d', category: STRUCTURAL },
44
+ { name: 'HAMAP', category: FAMILIES },
45
+ { name: 'PrositeProfiles', category: FAMILIES },
46
+ { name: 'PrositePatterns', category: FAMILIES },
47
+ { name: 'Coils', category: FEATURES },
48
+ { name: 'SMART', category: FAMILIES },
49
+ {
50
+ name: 'CDD',
51
+ category: FAMILIES,
52
+ description: 'Conserved Domains Database',
53
+ },
54
+ { name: 'PRINTS', category: FAMILIES },
55
+ { name: 'PfamA', category: FAMILIES },
56
+ { name: 'MobiDBLite', category: FEATURES },
57
+ { name: 'PIRSF', category: OTHER },
58
+ { name: 'TMHMM', category: FEATURES },
59
+ { name: 'AntiFam', category: OTHER },
60
+ { name: 'FunFam', category: OTHER },
61
+ { name: 'PIRSR', category: FAMILIES },
62
+ ].map(program => ({ ...program, checked: true }))
63
+
13
64
  const InterProScanDialog = observer(function ({
14
65
  handleClose,
15
66
  model,
@@ -17,134 +68,7 @@ const InterProScanDialog = observer(function ({
17
68
  handleClose: () => void
18
69
  model: MsaViewModel
19
70
  }) {
20
- const [vals, setVals] = useState([
21
- {
22
- name: 'NCBIfam',
23
- description: 'NCBI RefSeq FAMs including TIGRFAMs',
24
- category: 'Families, domains, sites & repeats',
25
- checked: true,
26
- },
27
- {
28
- name: 'SFLD',
29
- description: 'Structure function linkage database',
30
- category: 'Families, domains, sites & repeats',
31
- checked: true,
32
- },
33
- {
34
- name: 'Phobius',
35
- checked: true,
36
- description:
37
- 'A combined transmembrane topology and signal peptide predictor',
38
- category: 'Other sequence features',
39
- },
40
- {
41
- name: 'SignalP',
42
- checked: true,
43
- category: 'Other sequence features',
44
- },
45
- {
46
- name: 'SignalP_EUK',
47
- category: 'Other category',
48
- checked: true,
49
- },
50
- {
51
- name: 'SignalP_GRAM_POSITIVE',
52
- category: 'Other category',
53
- checked: true,
54
- },
55
- {
56
- name: 'SignalP_GRAM_NEGATIVE',
57
- checked: true,
58
- category: 'Other category',
59
- },
60
- {
61
- name: 'SuperFamily',
62
- category: 'Structural domains',
63
- checked: true,
64
- },
65
- {
66
- name: 'Panther',
67
- category: 'Families, domains, sites & repeats',
68
- checked: true,
69
- },
70
- {
71
- name: 'Gene3d',
72
- category: 'Structural domains',
73
- checked: true,
74
- },
75
- {
76
- name: 'HAMAP',
77
- category: 'Families, domains, sites & repeats',
78
- checked: true,
79
- },
80
- {
81
- name: 'PrositeProfiles',
82
- category: 'Families, domains, sites & repeats',
83
- checked: true,
84
- },
85
- {
86
- name: 'PrositePatterns',
87
- category: 'Families, domains, sites & repeats',
88
- checked: true,
89
- },
90
- {
91
- name: 'Coils',
92
- category: 'Other sequence features',
93
- checked: true,
94
- },
95
- {
96
- name: 'SMART',
97
- category: 'Families, domains, sites & repeats',
98
- checked: true,
99
- },
100
- {
101
- name: 'CDD',
102
- description: 'Conserved Domains Database',
103
- category: 'Families, domains, sites & repeats',
104
- checked: true,
105
- },
106
- {
107
- name: 'PRINTS',
108
- category: 'Families, domains, sites & repeats',
109
- checked: true,
110
- },
111
- {
112
- name: 'PfamA',
113
- category: 'Families, domains, sites & repeats',
114
- checked: true,
115
- },
116
- {
117
- name: 'MobiDBLite',
118
- checked: true,
119
- category: 'Other sequence features',
120
- },
121
- {
122
- name: 'PIRSF',
123
- checked: true,
124
- category: 'Other category',
125
- },
126
- {
127
- name: 'TMHMM',
128
- checked: true,
129
- category: 'Other sequence features',
130
- },
131
-
132
- {
133
- name: 'AntiFam',
134
- checked: true,
135
- category: 'Other category',
136
- },
137
- {
138
- name: 'FunFam',
139
- checked: true,
140
- category: 'Other category',
141
- },
142
- {
143
- name: 'PIRSR',
144
- checked: true,
145
- category: 'Families, domains, sites & repeats',
146
- },
147
- ])
71
+ const [vals, setVals] = useState(interProScanPrograms)
148
72
 
149
73
  const programs = vals.filter(e => e.checked).map(e => e.name)
150
74
  const [show, setShow] = useState(false)
@@ -197,12 +121,11 @@ const InterProScanDialog = observer(function ({
197
121
  <tbody>
198
122
  {vals
199
123
  .toSorted((a, b) => a.category.localeCompare(b.category))
200
- .map(({ name, checked, category }) => (
124
+ .map(({ name, checked, category, description }) => (
201
125
  <tr key={name}>
202
126
  <td>
203
127
  <input
204
128
  type="checkbox"
205
- key={name}
206
129
  checked={checked}
207
130
  onChange={() => {
208
131
  setVals(
@@ -215,7 +138,7 @@ const InterProScanDialog = observer(function ({
215
138
  }}
216
139
  />
217
140
  </td>
218
- <td>{name}</td>
141
+ <td title={description}>{name}</td>
219
142
  <td>{category}</td>
220
143
  </tr>
221
144
  ))}
@@ -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
  }),
@@ -47,7 +65,7 @@ export function treeSettingsMenuItems(model: MsaViewModel): MenuItem[] {
47
65
  toggle('Show tree', drawTree, arg => {
48
66
  model.setDrawTree(arg)
49
67
  }),
50
- toggle('Draw clickable bubbles on tree branches', drawNodeBubbles, arg => {
68
+ toggle('Draw bubbles on tree branches', drawNodeBubbles, arg => {
51
69
  model.setDrawNodeBubbles(arg)
52
70
  }),
53
71
  toggle('Tree labels align right', labelsAlignRight, arg => {