react-msaview 5.9.0 → 6.0.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 (196) 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/components/ConservationTrack.js +21 -16
  6. package/dist/components/ConservationTrack.js.map +1 -1
  7. package/dist/components/CopyButton.d.ts +9 -0
  8. package/dist/components/CopyButton.js +23 -0
  9. package/dist/components/CopyButton.js.map +1 -0
  10. package/dist/components/Loading.js +2 -2
  11. package/dist/components/Loading.js.map +1 -1
  12. package/dist/components/MSAView.js +18 -24
  13. package/dist/components/MSAView.js.map +1 -1
  14. package/dist/components/ResizeHandles.js +24 -40
  15. package/dist/components/ResizeHandles.js.map +1 -1
  16. package/dist/components/ScrollbarThumb.d.ts +14 -0
  17. package/dist/components/ScrollbarThumb.js +28 -0
  18. package/dist/components/ScrollbarThumb.js.map +1 -0
  19. package/dist/components/SequenceTextArea.js +3 -10
  20. package/dist/components/SequenceTextArea.js.map +1 -1
  21. package/dist/components/TextTrack.js +26 -21
  22. package/dist/components/TextTrack.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/{UserProvidedDomainsDialog.d.ts → InterProScanFileDialog.d.ts} +2 -2
  28. package/dist/components/dialogs/{UserProvidedDomainsDialog.js → InterProScanFileDialog.js} +6 -10
  29. package/dist/components/dialogs/InterProScanFileDialog.js.map +1 -0
  30. package/dist/components/dialogs/TrackInfoDialog.d.ts +2 -4
  31. package/dist/components/dialogs/TrackInfoDialog.js +8 -14
  32. package/dist/components/dialogs/TrackInfoDialog.js.map +1 -1
  33. package/dist/components/header/FileMenu.js +3 -3
  34. package/dist/components/header/FileMenu.js.map +1 -1
  35. package/dist/components/header/GappynessSlider.js +1 -1
  36. package/dist/components/header/GappynessSlider.js.map +1 -1
  37. package/dist/components/header/{getDomainsMenu.d.ts → getAnnotationsMenu.d.ts} +1 -1
  38. package/dist/components/header/{getDomainsMenu.js → getAnnotationsMenu.js} +10 -9
  39. package/dist/components/header/getAnnotationsMenu.js.map +1 -0
  40. package/dist/components/header/settingsMenuItems.js +39 -72
  41. package/dist/components/header/settingsMenuItems.js.map +1 -1
  42. package/dist/components/import/ImportForm.js +1 -1
  43. package/dist/components/import/ImportForm.js.map +1 -1
  44. package/dist/components/import/ImportFormExamples.js +66 -57
  45. package/dist/components/import/ImportFormExamples.js.map +1 -1
  46. package/dist/components/minimap/Minimap.js +11 -22
  47. package/dist/components/minimap/Minimap.js.map +1 -1
  48. package/dist/components/minimap/MinimapSVG.d.ts +2 -1
  49. package/dist/components/minimap/MinimapSVG.js +3 -4
  50. package/dist/components/minimap/MinimapSVG.js.map +1 -1
  51. package/dist/components/minimap/minimapLayout.d.ts +10 -1
  52. package/dist/components/minimap/minimapLayout.js +15 -10
  53. package/dist/components/minimap/minimapLayout.js.map +1 -1
  54. package/dist/components/msa/{DomainLegend.d.ts → AnnotationLegend.d.ts} +2 -2
  55. package/dist/components/msa/{DomainLegend.js → AnnotationLegend.js} +4 -4
  56. package/dist/components/msa/AnnotationLegend.js.map +1 -0
  57. package/dist/components/msa/MSACanvas.js +2 -2
  58. package/dist/components/msa/MSACanvas.js.map +1 -1
  59. package/dist/components/msa/MSACanvasBlock.js +19 -16
  60. package/dist/components/msa/MSACanvasBlock.js.map +1 -1
  61. package/dist/components/msa/MSAMouseoverCanvas.js +14 -8
  62. package/dist/components/msa/MSAMouseoverCanvas.js.map +1 -1
  63. package/dist/components/msa/MSAPanel.js +2 -2
  64. package/dist/components/msa/MSAPanel.js.map +1 -1
  65. package/dist/components/msa/domainBandCursor.d.ts +17 -0
  66. package/dist/components/msa/domainBandCursor.js +39 -0
  67. package/dist/components/msa/domainBandCursor.js.map +1 -0
  68. package/dist/components/msa/renderBoxFeatureCanvasBlock.js +9 -6
  69. package/dist/components/msa/renderBoxFeatureCanvasBlock.js.map +1 -1
  70. package/dist/components/msa/renderMSABlock.js +14 -20
  71. package/dist/components/msa/renderMSABlock.js.map +1 -1
  72. package/dist/components/msa/renderMSAMouseover.js +32 -33
  73. package/dist/components/msa/renderMSAMouseover.js.map +1 -1
  74. package/dist/components/overlayColors.d.ts +2 -0
  75. package/dist/components/overlayColors.js +5 -0
  76. package/dist/components/overlayColors.js.map +1 -1
  77. package/dist/components/tree/TreeCanvas.js +29 -24
  78. package/dist/components/tree/TreeCanvas.js.map +1 -1
  79. package/dist/components/tree/TreeCanvasBlock.js +19 -12
  80. package/dist/components/tree/TreeCanvasBlock.js.map +1 -1
  81. package/dist/components/tree/TreeRuler.js +5 -2
  82. package/dist/components/tree/TreeRuler.js.map +1 -1
  83. package/dist/components/tree/renderTreeCanvas.d.ts +0 -38
  84. package/dist/components/tree/renderTreeCanvas.js +17 -17
  85. package/dist/components/tree/renderTreeCanvas.js.map +1 -1
  86. package/dist/ggplotPalettes.d.ts +0 -1
  87. package/dist/ggplotPalettes.js +0 -3
  88. package/dist/ggplotPalettes.js.map +1 -1
  89. package/dist/hierarchy.d.ts +2 -6
  90. package/dist/hierarchy.js +4 -6
  91. package/dist/hierarchy.js.map +1 -1
  92. package/dist/index.d.ts +2 -2
  93. package/dist/launchInterProScan.d.ts +0 -23
  94. package/dist/launchInterProScan.js +2 -5
  95. package/dist/launchInterProScan.js.map +1 -1
  96. package/dist/measureTextCanvas.js +21 -6
  97. package/dist/measureTextCanvas.js.map +1 -1
  98. package/dist/model.d.ts +50 -100
  99. package/dist/model.js +209 -219
  100. package/dist/model.js.map +1 -1
  101. package/dist/neighborJoining.js +12 -19
  102. package/dist/neighborJoining.js.map +1 -1
  103. package/dist/parseAsn1.js +32 -43
  104. package/dist/parseAsn1.js.map +1 -1
  105. package/dist/renderToSvg.js +5 -2
  106. package/dist/renderToSvg.js.map +1 -1
  107. package/dist/rowCoordinateCalculations.d.ts +13 -17
  108. package/dist/rowCoordinateCalculations.js +45 -39
  109. package/dist/rowCoordinateCalculations.js.map +1 -1
  110. package/dist/seqPosToGlobalCol.d.ts +6 -19
  111. package/dist/seqPosToGlobalCol.js +6 -33
  112. package/dist/seqPosToGlobalCol.js.map +1 -1
  113. package/dist/types.d.ts +4 -11
  114. package/dist/useCanvasAutorun.d.ts +23 -1
  115. package/dist/useCanvasAutorun.js +20 -8
  116. package/dist/useCanvasAutorun.js.map +1 -1
  117. package/dist/version.d.ts +1 -1
  118. package/dist/version.js +1 -1
  119. package/package.json +5 -5
  120. package/src/clustalX.ts +22 -22
  121. package/src/colSpace.test.ts +27 -0
  122. package/src/components/ConservationTrack.tsx +7 -5
  123. package/src/components/CopyButton.tsx +44 -0
  124. package/src/components/Loading.tsx +10 -1
  125. package/src/components/MSAView.tsx +25 -32
  126. package/src/components/ResizeHandles.tsx +60 -69
  127. package/src/components/ScrollbarThumb.tsx +48 -0
  128. package/src/components/SequenceTextArea.tsx +3 -16
  129. package/src/components/TextTrack.tsx +7 -12
  130. package/src/components/VerticalScrollbar.tsx +19 -33
  131. package/src/components/dialogs/FeatureDialog.tsx +3 -4
  132. package/src/components/dialogs/{UserProvidedDomainsDialog.tsx → InterProScanFileDialog.tsx} +6 -14
  133. package/src/components/dialogs/TrackInfoDialog.tsx +18 -20
  134. package/src/components/header/FileMenu.tsx +3 -3
  135. package/src/components/header/GappynessSlider.tsx +4 -1
  136. package/src/components/header/{getDomainsMenu.ts → getAnnotationsMenu.ts} +10 -9
  137. package/src/components/header/settingsMenuItems.ts +40 -72
  138. package/src/components/import/ImportForm.tsx +1 -1
  139. package/src/components/import/ImportFormExamples.tsx +82 -133
  140. package/src/components/minimap/Minimap.tsx +23 -31
  141. package/src/components/minimap/MinimapSVG.tsx +32 -31
  142. package/src/components/minimap/minimapLayout.test.ts +60 -0
  143. package/src/components/minimap/minimapLayout.ts +15 -10
  144. package/src/components/msa/{DomainLegend.tsx → AnnotationLegend.tsx} +7 -3
  145. package/src/components/msa/MSACanvas.tsx +3 -10
  146. package/src/components/msa/MSACanvasBlock.tsx +16 -21
  147. package/src/components/msa/MSAMouseoverCanvas.tsx +11 -9
  148. package/src/components/msa/MSAPanel.tsx +2 -2
  149. package/src/components/msa/domainBandCursor.ts +41 -0
  150. package/src/components/msa/renderBoxFeatureCanvasBlock.test.ts +51 -0
  151. package/src/components/msa/renderBoxFeatureCanvasBlock.ts +13 -8
  152. package/src/components/msa/renderMSABlock.ts +19 -23
  153. package/src/components/msa/renderMSAMouseover.ts +38 -35
  154. package/src/components/overlayColors.ts +6 -0
  155. package/src/components/tree/TreeCanvas.tsx +6 -4
  156. package/src/components/tree/TreeCanvasBlock.tsx +11 -12
  157. package/src/components/tree/TreeRuler.tsx +7 -2
  158. package/src/components/tree/renderTreeCanvas.test.ts +15 -7
  159. package/src/components/tree/renderTreeCanvas.ts +20 -17
  160. package/src/domainBands.test.ts +24 -0
  161. package/src/domainLetterColor.test.ts +80 -0
  162. package/src/ggplotPalettes.ts +0 -4
  163. package/src/hierarchy.test.ts +7 -7
  164. package/src/hierarchy.ts +6 -9
  165. package/src/index.ts +4 -1
  166. package/src/largeTree.test.ts +23 -2
  167. package/src/launchInterProScan.ts +3 -30
  168. package/src/loadErrorDisplay.test.tsx +102 -0
  169. package/src/measureTextCanvas.ts +22 -6
  170. package/src/model.ts +233 -244
  171. package/src/modelFilehandleLoaders.test.ts +137 -0
  172. package/src/neighborJoining.test.ts +12 -0
  173. package/src/neighborJoining.ts +12 -20
  174. package/src/parseAsn1.ts +32 -40
  175. package/src/{renderDomainLegendSvg.test.tsx → renderDomainsSvg.test.tsx} +44 -20
  176. package/src/renderMinimapSvg.test.tsx +109 -0
  177. package/src/renderToSvg.tsx +5 -2
  178. package/src/rowCoordinateCalculations.ts +48 -44
  179. package/src/seqPosToGlobalCol.test.ts +39 -51
  180. package/src/seqPosToGlobalCol.ts +6 -41
  181. package/src/types.ts +10 -15
  182. package/src/useCanvasAutorun.test.tsx +98 -0
  183. package/src/useCanvasAutorun.ts +30 -11
  184. package/src/version.ts +1 -1
  185. package/src/viewChrome.test.tsx +134 -0
  186. package/dist/components/dialogs/DomainDialog.d.ts +0 -6
  187. package/dist/components/dialogs/DomainDialog.js +0 -19
  188. package/dist/components/dialogs/DomainDialog.js.map +0 -1
  189. package/dist/components/dialogs/UserProvidedDomainsDialog.js.map +0 -1
  190. package/dist/components/header/SettingsMenu.d.ts +0 -6
  191. package/dist/components/header/SettingsMenu.js +0 -40
  192. package/dist/components/header/SettingsMenu.js.map +0 -1
  193. package/dist/components/header/getDomainsMenu.js.map +0 -1
  194. package/dist/components/msa/DomainLegend.js.map +0 -1
  195. package/src/components/dialogs/DomainDialog.tsx +0 -45
  196. package/src/components/header/SettingsMenu.tsx +0 -57
@@ -31,24 +31,17 @@ const MSACanvasBlock = observer(function ({
31
31
  },
32
32
  )
33
33
 
34
- const ref = useCanvasAutorun(
35
- ctx => {
36
- const { blockSize, highResScaleFactor, actuallyShowDomains } = model
34
+ const canvasSize = blockSize * highResScaleFactor
35
+ const ref = useCanvasAutorun({
36
+ draw: ctx => {
37
37
  ctx.resetTransform()
38
- ctx.clearRect(
39
- 0,
40
- 0,
41
- blockSize * highResScaleFactor,
42
- blockSize * highResScaleFactor,
43
- )
44
- if (actuallyShowDomains) {
45
- renderBoxFeatureCanvasBlock({
46
- ctx,
47
- offsetX,
48
- offsetY,
49
- model,
50
- })
51
- }
38
+ ctx.clearRect(0, 0, canvasSize, canvasSize)
39
+ renderBoxFeatureCanvasBlock({
40
+ ctx,
41
+ offsetX,
42
+ offsetY,
43
+ model,
44
+ })
52
45
  renderMSABlock({
53
46
  ctx,
54
47
  theme,
@@ -58,8 +51,10 @@ const MSACanvasBlock = observer(function ({
58
51
  model,
59
52
  })
60
53
  },
61
- [model, offsetX, offsetY, theme, contrastScheme],
62
- )
54
+ width: canvasSize,
55
+ height: canvasSize,
56
+ deps: [model, offsetX, offsetY, theme, contrastScheme],
57
+ })
63
58
 
64
59
  const { hoveredInsertion, mouseOverDomains, showColumnStats } = model
65
60
 
@@ -80,8 +75,8 @@ const MSACanvasBlock = observer(function ({
80
75
  onMouseLeave={() => {
81
76
  onMouseLeave()
82
77
  }}
83
- width={blockSize * highResScaleFactor}
84
- height={blockSize * highResScaleFactor}
78
+ width={canvasSize}
79
+ height={canvasSize}
85
80
  style={{
86
81
  position: 'absolute',
87
82
  top: scrollY + offsetY,
@@ -13,24 +13,26 @@ const MSAMouseoverCanvas = observer(function ({
13
13
  }: {
14
14
  model: MsaViewModel
15
15
  }) {
16
- const { height, msaAreaWidth, verticalScrollbarWidth, highResScaleFactor } =
17
- model
18
- const width = msaAreaWidth - verticalScrollbarWidth
19
- const ref = useCanvasAutorun(
20
- ctx => {
16
+ const { height, msaCanvasWidth: width, highResScaleFactor } = model
17
+ const canvasWidth = width * highResScaleFactor
18
+ const canvasHeight = height * highResScaleFactor
19
+ const ref = useCanvasAutorun({
20
+ draw: ctx => {
21
21
  if (isAlive(model)) {
22
22
  renderMouseover({ ctx, model })
23
23
  }
24
24
  },
25
- [model],
26
- )
25
+ width: canvasWidth,
26
+ height: canvasHeight,
27
+ deps: [model],
28
+ })
27
29
 
28
30
  return (
29
31
  <canvas
30
32
  ref={ref}
31
33
  id="mouseover"
32
- width={width * highResScaleFactor}
33
- height={height * highResScaleFactor}
34
+ width={canvasWidth}
35
+ height={canvasHeight}
34
36
  style={{
35
37
  position: 'absolute',
36
38
  top: 0,
@@ -2,7 +2,7 @@ import React from 'react'
2
2
 
3
3
  import { observer } from 'mobx-react'
4
4
 
5
- import DomainLegend from './DomainLegend.tsx'
5
+ import AnnotationLegend from './AnnotationLegend.tsx'
6
6
  import MSACanvas from './MSACanvas.tsx'
7
7
  import MSAMouseoverCanvas from './MSAMouseoverCanvas.tsx'
8
8
 
@@ -14,7 +14,7 @@ const MSAPanel = observer(function ({ model }: { model: MsaViewModel }) {
14
14
  <div style={{ position: 'relative' }}>
15
15
  <MSACanvas model={model} />
16
16
  <MSAMouseoverCanvas model={model} />
17
- <DomainLegend model={model} />
17
+ <AnnotationLegend model={model} />
18
18
  </div>
19
19
  )
20
20
  })
@@ -0,0 +1,41 @@
1
+ import type { DomainBand } from '../../types.ts'
2
+
3
+ /**
4
+ * Left-to-right cursor over one row's domain bands, answering "which band is
5
+ * drawn on top at this column" — the fill a letter there has to contrast
6
+ * against.
7
+ *
8
+ * Not simply the first band covering the column. The overlay paints in
9
+ * `domainBands` order, largest first, so where signatures nest — a family
10
+ * enclosing a domain, which InterProScan emits routinely — the box a reader
11
+ * actually sees is the last one painted over that column. `stackIndex` is that
12
+ * paint order.
13
+ *
14
+ * `bands` must be sorted by start column (`domainBandsByStart`), and columns
15
+ * must be visited in increasing order, which is what the block renderer does.
16
+ * Both keep this to one pass over the bands per row rather than a scan per cell.
17
+ */
18
+ export function domainBandCursor(bands: DomainBand[] | undefined) {
19
+ let next = 0
20
+ let active: DomainBand[] = []
21
+
22
+ return (col: number) => {
23
+ if (!bands) {
24
+ return undefined
25
+ }
26
+ while (next < bands.length && bands[next]!.startCol <= col) {
27
+ active.push(bands[next]!)
28
+ next++
29
+ }
30
+ if (active.some(band => band.endCol <= col)) {
31
+ active = active.filter(band => band.endCol > col)
32
+ }
33
+ let top: DomainBand | undefined
34
+ for (const band of active) {
35
+ if (!top || band.stackIndex > top.stackIndex) {
36
+ top = band
37
+ }
38
+ }
39
+ return top
40
+ }
41
+ }
@@ -0,0 +1,51 @@
1
+ import { expect, test } from 'vitest'
2
+
3
+ import stateModelFactory from '../../model.ts'
4
+ import { renderBoxFeatureCanvasBlock } from './renderBoxFeatureCanvasBlock.ts'
5
+
6
+ import type { RenderCtx } from '../renderCtx.ts'
7
+
8
+ const row = 'ACDEFGHIKLMNPQRSTVWY'.repeat(3)
9
+
10
+ // only `human` carries the gene model; `mouse` is there to push it off the top
11
+ // of the block
12
+ const msa = `>mouse\n${row}\n>human\n${row}`
13
+
14
+ const gff = `##gff-version 3
15
+ human src exon 1 20 . . . ID=human.exon1;Name=exon-1
16
+ human src exon 21 40 . . . ID=human.exon2;Name=exon-2
17
+ human src exon 41 60 . . . ID=human.exon3;Name=exon-3`
18
+
19
+ function drawnLabels() {
20
+ const model = stateModelFactory().create({
21
+ type: 'MsaView',
22
+ data: { msa },
23
+ })
24
+ model.setWidth(1000)
25
+ model.setRowHeight(20)
26
+ // segment numbers are drawn in place of residue letters, never on top of them
27
+ model.setDrawMsaLetters(false)
28
+ model.applyGFFText(gff)
29
+ expect(model.rowNames).toEqual(['mouse', 'human'])
30
+
31
+ const labels: string[] = []
32
+ const ctx = {
33
+ font: '12px sans-serif',
34
+ resetTransform() {},
35
+ scale() {},
36
+ translate() {},
37
+ fillRect() {},
38
+ strokeRect() {},
39
+ measureText: (text: string) => ({ width: text.length * 6 }),
40
+ fillText(text: string) {
41
+ labels.push(text)
42
+ },
43
+ } as unknown as RenderCtx
44
+
45
+ renderBoxFeatureCanvasBlock({ model, ctx, offsetX: 0, offsetY: 0 })
46
+ return labels
47
+ }
48
+
49
+ test('segment numbers are drawn once each, even when row 0 has no gene model', () => {
50
+ expect(drawnLabels()).toEqual(['1', '2', '3'])
51
+ })
@@ -77,8 +77,13 @@ function drawTiles({
77
77
  const xMin = offsetX - cull
78
78
  const xMax = offsetX + blockWidth + cull
79
79
 
80
- for (let i = 0, l1 = visibleLeaves.length; i < l1; i++) {
81
- const node = visibleLeaves[i]!
80
+ // a segment (exon) number labels its band once per block, on the topmost
81
+ // visible row carrying that segment, so it reads as a column header for the
82
+ // whole band. Keyed by accession rather than drawn on row 0, because the rows
83
+ // carrying the gene model are often not the first ones in the alignment
84
+ const labelled = drawSegmentLabels ? new Set<string>() : undefined
85
+
86
+ for (const node of visibleLeaves) {
82
87
  const y = node.x!
83
88
  const bands = domainBands.get(node.data.name)
84
89
 
@@ -94,15 +99,15 @@ function drawTiles({
94
99
  if (strand === undefined) {
95
100
  ctx.fillRect(x, t, lw, h)
96
101
  ctx.strokeRect(x, t, lw, h)
97
- // segment (exon) number drawn once per band, on the top visible row,
98
- // so it reads as a column header for the whole band
99
- const label = drawSegmentLabels
100
- ? segmentLabels.get(accession)
101
- : undefined
102
- if (label !== undefined && i === 0) {
102
+ const label =
103
+ labelled && !labelled.has(accession)
104
+ ? segmentLabels.get(accession)
105
+ : undefined
106
+ if (label !== undefined) {
103
107
  const fontSize = Math.min(h - 2, 11)
104
108
  ctx.font = `${fontSize}px sans-serif`
105
109
  if (ctx.measureText(label).width + 2 <= lw) {
110
+ labelled!.add(accession)
106
111
  ctx.fillStyle = '#222'
107
112
  ctx.textAlign = 'center'
108
113
  ctx.textBaseline = 'middle'
@@ -1,6 +1,7 @@
1
1
  import { setFontSize } from '../../setFontSize.ts'
2
2
  import { adjustColorForContrast } from '../../util.ts'
3
3
  import { getVisibleLeaves } from '../getVisibleLeaves.ts'
4
+ import { domainBandCursor } from './domainBandCursor.ts'
4
5
  import { visibleColRange } from './visibleColRange.ts'
5
6
 
6
7
  import type { HierarchyNode } from '../../hierarchy.ts'
@@ -167,8 +168,9 @@ function drawTilesAndText({
167
168
  if (str) {
168
169
  const tileY = y - rowHeight
169
170
  const textY = y - quarterRowHeight
170
- const bands = overDomains ? domainBandsByStart.get(name) : undefined
171
- let band = 0
171
+ const bandAt = domainBandCursor(
172
+ overDomains ? domainBandsByStart.get(name) : undefined,
173
+ )
172
174
 
173
175
  for (let j = 0, l2 = str.length; j < l2; j++) {
174
176
  const col = xStart + j
@@ -186,27 +188,21 @@ function drawTilesAndText({
186
188
  }
187
189
 
188
190
  if (showMsaLetters) {
189
- // bands are sorted by start column and the sweep is left to right,
190
- // so one forward-only cursor finds the band covering each column
191
- while (bands && band < bands.length && bands[band]!.endCol <= col) {
192
- band++
193
- }
194
- const covering = bands?.[band]
195
- ctx.fillStyle =
196
- covering && covering.startCol <= col
197
- ? // on top of a domain box: contrast against the box fill
198
- domainColors!.get(covering.annotation.accession)!
199
- : tiles
200
- ? // on top of a colored tile
201
- (contrastScheme[letter] ?? 'black')
202
- : !drawTiles || !color
203
- ? // plain background, uncolored letters
204
- theme.palette.text.primary
205
- : // letter-color mode: darken/lighten to stay readable
206
- adjustColorForContrast(
207
- color,
208
- theme.palette.background.default,
209
- )
191
+ const covering = bandAt(col)
192
+ ctx.fillStyle = covering
193
+ ? // on top of a domain box: contrast against the box fill
194
+ domainColors!.get(covering.annotation.accession)!
195
+ : tiles
196
+ ? // on top of a colored tile
197
+ (contrastScheme[letter] ?? 'black')
198
+ : !drawTiles || !color
199
+ ? // plain background, uncolored letters
200
+ theme.palette.text.primary
201
+ : // letter-color mode: darken/lighten to stay readable
202
+ adjustColorForContrast(
203
+ color,
204
+ theme.palette.background.default,
205
+ )
210
206
  ctx.fillText(
211
207
  isMatchingReference ? '.' : letter,
212
208
  x + halfColWidth,
@@ -1,9 +1,12 @@
1
- import { multiRowHoverColor, referenceColor } from '../overlayColors.ts'
1
+ import {
2
+ clickColor,
3
+ hoverColor,
4
+ multiRowHoverColor,
5
+ referenceColor,
6
+ } from '../overlayColors.ts'
2
7
 
3
8
  import type { MsaViewModel } from '../../model.ts'
4
9
 
5
- const hoverColor = 'rgba(0,0,0,0.15)'
6
- const highlightColor = 'rgba(128,128,0,0.2)'
7
10
  // the persistent highlightColumns overlay: a stronger fill plus a solid border
8
11
  // so a domain/motif band reads clearly over the colored alignment cells (the
9
12
  // faint hover-style wash alone is invisible against clustalx coloring)
@@ -20,8 +23,7 @@ export function renderMouseover({
20
23
  const {
21
24
  mouseCol,
22
25
  colWidth,
23
- msaAreaWidth,
24
- verticalScrollbarWidth,
26
+ msaCanvasWidth: width,
25
27
  height,
26
28
  rowHeight,
27
29
  scrollX,
@@ -34,53 +36,54 @@ export function renderMouseover({
34
36
  hoveredRowIndices,
35
37
  highlightedColumnRuns,
36
38
  } = model
37
- const width = msaAreaWidth - verticalScrollbarWidth
38
39
  ctx.resetTransform()
39
40
  ctx.clearRect(0, 0, width * highResScaleFactor, height * highResScaleFactor)
40
41
  ctx.scale(highResScaleFactor, highResScaleFactor)
41
42
 
42
- // Highlight reference row (relativeTo) persistently
43
+ // rows and columns sit at a fixed pitch, so an index is all it takes to place
44
+ // a full-width or full-height band (matching fillRow in TreeCanvas)
45
+ const rowBand = (index: number) => {
46
+ ctx.fillRect(0, index * rowHeight + scrollY, width, rowHeight)
47
+ }
48
+ const colBand = (index: number) => {
49
+ ctx.fillRect(index * colWidth + scrollX, 0, colWidth, height)
50
+ }
51
+
52
+ // the reference row (relativeTo) stays lit, as does every tip under a hovered
53
+ // tree node
43
54
  if (referenceRowIndex !== undefined) {
44
55
  ctx.fillStyle = referenceColor
45
- ctx.fillRect(0, referenceRowIndex * rowHeight + scrollY, width, rowHeight)
56
+ rowBand(referenceRowIndex)
46
57
  }
47
-
48
- // Highlight multiple rows when hovering over tree nodes with children
49
- if (hoveredRowIndices.length > 0) {
50
- ctx.fillStyle = multiRowHoverColor
51
- for (const rowIndex of hoveredRowIndices) {
52
- ctx.fillRect(0, rowIndex * rowHeight + scrollY, width, rowHeight)
53
- }
58
+ ctx.fillStyle = multiRowHoverColor
59
+ for (const rowIndex of hoveredRowIndices) {
60
+ rowBand(rowIndex)
54
61
  }
55
62
 
56
- // Highlight multiple columns draw each contiguous run as a filled, bordered
57
- // band so a domain/motif highlight is clearly visible over the alignment
58
- if (highlightedColumnRuns.length > 0) {
59
- ctx.lineWidth = 2
60
- for (const { start, end } of highlightedColumnRuns) {
61
- const x = start * colWidth + scrollX
62
- const w = (end - start + 1) * colWidth
63
- ctx.fillStyle = highlightColumnsFill
64
- ctx.fillRect(x, 0, w, height)
65
- ctx.strokeStyle = highlightColumnsBorder
66
- ctx.strokeRect(x, 0, w, height)
67
- }
63
+ // each contiguous run of highlighted columns draws as one filled, bordered
64
+ // band, so a domain/motif highlight stays visible over the alignment
65
+ ctx.lineWidth = 2
66
+ for (const { start, end } of highlightedColumnRuns) {
67
+ const x = start * colWidth + scrollX
68
+ const w = (end - start + 1) * colWidth
69
+ ctx.fillStyle = highlightColumnsFill
70
+ ctx.fillRect(x, 0, w, height)
71
+ ctx.strokeStyle = highlightColumnsBorder
72
+ ctx.strokeRect(x, 0, w, height)
68
73
  }
69
74
 
75
+ ctx.fillStyle = hoverColor
70
76
  if (mouseCol !== undefined) {
71
- ctx.fillStyle = hoverColor
72
- ctx.fillRect(mouseCol * colWidth + scrollX, 0, colWidth, height)
77
+ colBand(mouseCol)
73
78
  }
74
79
  if (mouseRow !== undefined) {
75
- ctx.fillStyle = hoverColor
76
- ctx.fillRect(0, mouseRow * rowHeight + scrollY, width, rowHeight)
80
+ rowBand(mouseRow)
77
81
  }
82
+ ctx.fillStyle = clickColor
78
83
  if (mouseClickCol !== undefined) {
79
- ctx.fillStyle = highlightColor
80
- ctx.fillRect(mouseClickCol * colWidth + scrollX, 0, colWidth, height)
84
+ colBand(mouseClickCol)
81
85
  }
82
86
  if (mouseClickRow !== undefined) {
83
- ctx.fillStyle = highlightColor
84
- ctx.fillRect(0, mouseClickRow * rowHeight + scrollY, width, rowHeight)
87
+ rowBand(mouseClickRow)
85
88
  }
86
89
  }
@@ -10,3 +10,9 @@ export const referenceColor = 'rgba(0,128,255,0.3)'
10
10
  // while the alignment cells are already colored so a lighter wash reads better.
11
11
  export const treeHoverColor = 'rgba(255,165,0,0.2)'
12
12
  export const multiRowHoverColor = 'rgba(255,165,0,0.15)'
13
+
14
+ // the hovered column/row, and the one pinned by a click. Drawn on the alignment
15
+ // overlay canvas and again as plain divs over the track area, which have to
16
+ // match or the crosshair appears to change color as it crosses the tracks.
17
+ export const hoverColor = 'rgba(0,0,0,0.15)'
18
+ export const clickColor = 'rgba(128,128,0,0.2)'
@@ -22,8 +22,8 @@ const TreeCanvas = observer(function ({ model }: { model: MsaViewModel }) {
22
22
  )
23
23
  const { onMouseDown, onMouseUp } = useWheelScroll({ ref, onScrollY })
24
24
 
25
- const mouseoverRef = useCanvasAutorun(
26
- ctx => {
25
+ const mouseoverRef = useCanvasAutorun({
26
+ draw: ctx => {
27
27
  if (isAlive(model)) {
28
28
  const {
29
29
  rowHeight,
@@ -61,8 +61,10 @@ const TreeCanvas = observer(function ({ model }: { model: MsaViewModel }) {
61
61
  }
62
62
  }
63
63
  },
64
- [model],
65
- )
64
+ width: treeAreaWidth,
65
+ height,
66
+ deps: [model],
67
+ })
66
68
 
67
69
  return (
68
70
  <div
@@ -60,15 +60,12 @@ const TreeCanvasBlock = observer(function ({
60
60
  const width = treeAreaWidth + padding
61
61
  const height = blockSize
62
62
 
63
- const ref = useCanvasAutorun(
64
- ctx => {
63
+ const canvasWidth = width * highResScaleFactor
64
+ const canvasHeight = height * highResScaleFactor
65
+ const ref = useCanvasAutorun({
66
+ draw: ctx => {
65
67
  ctx.resetTransform()
66
- ctx.clearRect(
67
- 0,
68
- 0,
69
- width * highResScaleFactor,
70
- height * highResScaleFactor,
71
- )
68
+ ctx.clearRect(0, 0, canvasWidth, canvasHeight)
72
69
  renderTreeCanvas({
73
70
  ctx,
74
71
  model,
@@ -77,8 +74,10 @@ const TreeCanvasBlock = observer(function ({
77
74
  theme,
78
75
  })
79
76
  },
80
- [model, clickMap, offsetY, theme],
81
- )
77
+ width: canvasWidth,
78
+ height: canvasHeight,
79
+ deps: [model, clickMap, offsetY, theme],
80
+ })
82
81
 
83
82
  const style = {
84
83
  width,
@@ -112,8 +111,8 @@ const TreeCanvasBlock = observer(function ({
112
111
 
113
112
  <canvas
114
113
  ref={ref}
115
- width={width * highResScaleFactor}
116
- height={height * highResScaleFactor}
114
+ width={canvasWidth}
115
+ height={canvasHeight}
117
116
  style={{ ...style, cursor: hovered ? 'pointer' : 'default' }}
118
117
  onMouseMove={event => {
119
118
  onMouseMove(event)
@@ -4,9 +4,14 @@ import { observer } from 'mobx-react'
4
4
 
5
5
  import type { MsaViewModel } from '../../model.ts'
6
6
 
7
+ // The gutter above the tree panel. It spans the resize handle too, so whatever
8
+ // follows it in the top row starts exactly where the alignment canvas does --
9
+ // MainArea has TreePanel + VerticalResizeHandle before the alignment.
7
10
  const TreeRuler = observer(({ model }: { model: MsaViewModel }) => {
8
- const { treeAreaWidth } = model
9
- return <div style={{ flexShrink: 0, width: treeAreaWidth }} />
11
+ const { treeAreaWidth, resizeHandleWidth } = model
12
+ return (
13
+ <div style={{ flexShrink: 0, width: treeAreaWidth + resizeHandleWidth }} />
14
+ )
10
15
  })
11
16
 
12
17
  export default TreeRuler
@@ -117,8 +117,9 @@ describe('getNodeX cladogram positioning', () => {
117
117
  })
118
118
  })
119
119
 
120
- // the x values the tree pass actually strokes, for a model built from newick
121
- function drawnTreeXs(newick: string) {
120
+ // the x values the tree pass actually strokes, for a model built from newick,
121
+ // alongside the tree width they are expected to span
122
+ function drawnTree(newick: string) {
122
123
  const model = stateModelFactory().create({
123
124
  type: 'MsaView',
124
125
  data: { msa: '>a\nA\n>b\nA\n>c\nA\n>d\nA', tree: newick },
@@ -150,19 +151,26 @@ function drawnTreeXs(newick: string) {
150
151
  offsetY: 0,
151
152
  theme: { palette: { text: { primary: '#000' } } } as Theme,
152
153
  })
153
- return xs
154
+ return { xs, model }
154
155
  }
155
156
 
156
157
  describe('renderTreeCanvas horizontal extent', () => {
157
158
  it('spreads a cladogram across the tree area when branch lengths are absent', () => {
158
159
  // a lengthless newick forces cladogram mode; scaling it by the (zero) max
159
160
  // branch length would stack every node on x=0 as one vertical line
160
- const xs = drawnTreeXs('((a,b),(c,d));')
161
- expect(Math.max(...xs)).toBe(300)
161
+ const { xs, model } = drawnTree('((a,b),(c,d));')
162
+ expect(Math.max(...xs)).toBe(model.treeWidth)
162
163
  })
163
164
 
164
165
  it('scales a phylogram by branch length', () => {
165
- const xs = drawnTreeXs('((a:0.1,b:0.2):0.3,(c:0.4,d:0.5):0.6);')
166
- expect(Math.max(...xs)).toBeCloseTo(300)
166
+ const { xs, model } = drawnTree('((a:0.1,b:0.2):0.3,(c:0.4,d:0.5):0.6);')
167
+ expect(Math.max(...xs)).toBeCloseTo(model.treeWidth)
168
+ })
169
+
170
+ // labels off means no label gutter to reserve, so the tips reach the far edge
171
+ // of the tree area rather than stopping at the default treeWidth
172
+ it('hands the label gutter to the tree when labels are off', () => {
173
+ const { model } = drawnTree('((a,b),(c,d));')
174
+ expect(model.treeWidth).toBe(model.treeAreaWidth - 10 - model.marginLeft)
167
175
  })
168
176
  })