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
@@ -3,6 +3,7 @@ import React, { useState } from 'react'
3
3
  import { makeStyles } from '@jbrowse/core/util/tss-react'
4
4
  import { useTheme } from '@mui/material'
5
5
  import { observer } from 'mobx-react'
6
+ import { createPortal } from 'react-dom'
6
7
 
7
8
  import { useCanvasAutorun } from '../../useCanvasAutorun.ts'
8
9
  import TreeBranchMenu from './TreeBranchMenu.tsx'
@@ -56,7 +57,7 @@ const TreeCanvasBlock = observer(function ({
56
57
  const { clickMap, hovered, hitTest, onMouseMove, onMouseLeave } =
57
58
  useTreeHover({ model, offsetY })
58
59
 
59
- const { scrollY, treeAreaWidth, blockSize, highResScaleFactor } = model
60
+ const { treeAreaWidth, blockSize, highResScaleFactor } = model
60
61
  const width = treeAreaWidth + padding
61
62
  const height = blockSize
62
63
 
@@ -82,7 +83,7 @@ const TreeCanvasBlock = observer(function ({
82
83
  const style = {
83
84
  width,
84
85
  height,
85
- top: scrollY + offsetY,
86
+ top: offsetY,
86
87
  left: 0,
87
88
  position: 'absolute',
88
89
  } as const
@@ -144,21 +145,27 @@ const TreeCanvasBlock = observer(function ({
144
145
  className={classes.hover}
145
146
  style={{
146
147
  left: hovered.minX,
147
- top: hovered.minY + scrollY,
148
+ top: hovered.minY,
148
149
  width: hovered.maxX - hovered.minX,
149
150
  height: hovered.maxY - hovered.minY,
150
151
  }}
151
152
  />
152
153
  ) : null}
153
154
 
154
- {hovered ? (
155
- <div
156
- className={classes.tooltip}
157
- style={{ left: hovered.clientX + 12, top: hovered.clientY + 12 }}
158
- >
159
- {hovered.name}
160
- </div>
161
- ) : null}
155
+ {/* portaled: the block set sits under a transform, which would otherwise
156
+ make this fixed tooltip position against the scrolled tree instead of the
157
+ window */}
158
+ {hovered
159
+ ? createPortal(
160
+ <div
161
+ className={classes.tooltip}
162
+ style={{ left: hovered.clientX + 12, top: hovered.clientY + 12 }}
163
+ >
164
+ {hovered.name}
165
+ </div>,
166
+ document.body,
167
+ )
168
+ : null}
162
169
  </>
163
170
  )
164
171
  })
@@ -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
  })
@@ -264,6 +264,7 @@ function renderTreeLabels({
264
264
  marginLeft,
265
265
  noTree,
266
266
  labelWidthMap,
267
+ labelWidthScale,
267
268
  } = model
268
269
  const by = blockSizeYOverride ?? blockSize
269
270
  // labels only exist for leaves, which are laid out top to bottom, so take the
@@ -295,8 +296,11 @@ function renderTreeLabels({
295
296
  xp = getNodeX(node, showBranchLen, tipX, maxDepthToLeaf) ?? 0
296
297
  }
297
298
 
299
+ const measured = labelWidthMap.get(name)
298
300
  const width =
299
- labelWidthMap.get(name) ?? ctx.measureText(displayName).width
301
+ measured === undefined
302
+ ? ctx.measureText(displayName).width
303
+ : measured * labelWidthScale
300
304
 
301
305
  ctx.fillStyle = theme.palette.text.primary
302
306
  if (labelsAlignRight) {
package/src/constants.ts CHANGED
@@ -31,6 +31,16 @@ export const segmentShades = ['#9fb6d4', '#d4dcea']
31
31
  export const defaultDrawMsaLetters = true
32
32
  export const defaultScrollZoom = false
33
33
 
34
+ // Cell size floors for drawing residue letters and tree labels. Below these a
35
+ // 500px block holds thousands of glyphs, and fillText -- which no sprite atlas
36
+ // beats, measured -- dominates every zoom frame.
37
+ export const minLetterRowHeight = 8
38
+ export const minLetterColWidth = 5
39
+
40
+ // Tree labels are measured once at this size and scaled to the current font
41
+ // size, so a vertical zoom never re-measures the tree.
42
+ export const labelReferenceFontSize = 16
43
+
34
44
  // Zoom limits, px per cell. maxCellSize caps both smooth zoom and the stepwise
35
45
  // zoom-in buttons (which otherwise grow unbounded). minColWidth/minRowHeight are
36
46
  // the smooth-zoom floors.
@@ -18,6 +18,12 @@ const gff = `##gff-version 3
18
18
  a Pfam protein_match 1 4 . . . Name=PF00001;signature_desc=First
19
19
  a Pfam protein_match 5 8 . . . Name=PF00002;signature_desc=Second`
20
20
 
21
+ // row `b` only: PF00003 ends inside the insertion, PF00004 lies entirely within
22
+ // it, so gap hiding puts one end and then both ends into hidden columns
23
+ const insertionGff = `##gff-version 3
24
+ b Pfam protein_match 4 8 . . . Name=PF00003;signature_desc=Overlapping
25
+ b Pfam protein_match 5 8 . . . Name=PF00004;signature_desc=Inside`
26
+
21
27
  function makeModel() {
22
28
  const model = MSAModelF().create({
23
29
  id: 'domain-bands-test',
@@ -56,6 +62,24 @@ test('bands follow the columns when gappy columns are hidden', () => {
56
62
  ])
57
63
  })
58
64
 
65
+ test('a band whose end lands in a hidden column keeps its visible part', () => {
66
+ const model = makeModel()
67
+ model.applyGFFText(insertionGff)
68
+ expect(model.domainBands.get('b')!.map(b => [b.startCol, b.endCol])).toEqual([
69
+ [3, 8],
70
+ [4, 8],
71
+ ])
72
+
73
+ model.setHideGaps(true)
74
+ model.setAllowedGappyness(50)
75
+ expect(model.blanks).toEqual([4, 5, 6, 7])
76
+ // PF00003 covers V + the 4 hidden Qs, so only the V column survives; PF00004
77
+ // is nothing but hidden columns and drops out entirely
78
+ expect(model.domainBands.get('b')!.map(b => [b.startCol, b.endCol])).toEqual([
79
+ [3, 4],
80
+ ])
81
+ })
82
+
59
83
  test('mouseOverDomains hit-tests against the drawn span', () => {
60
84
  const model = makeModel()
61
85
  const rowIndex = model.rowNames.indexOf('a')
@@ -0,0 +1,80 @@
1
+ // @vitest-environment jsdom
2
+ //
3
+ // Letters drawn on top of the domain overlay take their color from the box
4
+ // underneath them. The overlay paints bands largest-first, so with nested
5
+ // signatures -- routine in InterProScan output, where a family encloses a
6
+ // domain -- the box actually visible at a column is the *last* one painted,
7
+ // not the first one covering it.
8
+ import { expect, test } from 'vitest'
9
+
10
+ import { domainBandCursor } from './components/msa/domainBandCursor.ts'
11
+ import MSAModelF from './model.ts'
12
+
13
+ const row = 'ACDEFGHIKLMNPQRSTVWY'
14
+ const msa = `>seq1\n${row}\n>seq2\n${row}`
15
+
16
+ // PF_OUTER spans the whole row, PF_INNER sits inside it. tidyInterProAnnotations
17
+ // orders by length descending, so the outer band paints first and the inner one
18
+ // covers it.
19
+ const gff = `##gff-version 3
20
+ seq1\tPfam\tprotein_match\t1\t20\t.\t.\t.\tName=PF_OUTER;signature_desc=Outer
21
+ seq1\tPfam\tprotein_match\t8\t12\t.\t.\t.\tName=PF_INNER;signature_desc=Inner`
22
+
23
+ function makeModel() {
24
+ const model = MSAModelF().create({
25
+ type: 'MsaView',
26
+ msaFormat: 'fasta',
27
+ data: { msa },
28
+ })
29
+ model.setWidth(800)
30
+ model.applyGFFText(gff)
31
+ return model
32
+ }
33
+
34
+ function sweep(model: ReturnType<typeof makeModel>, from = 0) {
35
+ const bandAt = domainBandCursor(model.domainBandsByStart.get('seq1'))
36
+ const out: (string | undefined)[] = []
37
+ for (let col = from; col < 20; col++) {
38
+ out.push(bandAt(col)?.annotation.accession)
39
+ }
40
+ return out
41
+ }
42
+
43
+ test('the overlay paints largest-first, so stackIndex is paint order', () => {
44
+ const bands = makeModel().domainBands.get('seq1')!
45
+ expect(bands.map(b => [b.annotation.accession, b.stackIndex])).toEqual([
46
+ ['PF_OUTER', 0],
47
+ ['PF_INNER', 1],
48
+ ])
49
+ })
50
+
51
+ test('a nested band wins the columns it covers', () => {
52
+ const model = makeModel()
53
+ // PF_INNER is seq positions 8-12 (1-based), i.e. columns 7..11
54
+ expect(sweep(model)).toEqual([
55
+ ...Array.from({ length: 7 }, () => 'PF_OUTER'),
56
+ ...Array.from({ length: 5 }, () => 'PF_INNER'),
57
+ ...Array.from({ length: 8 }, () => 'PF_OUTER'),
58
+ ])
59
+ })
60
+
61
+ test('a sweep starting mid-block picks up bands opened before it', () => {
62
+ // canvas blocks start at arbitrary columns, so the cursor has to catch up on
63
+ // its first call rather than assuming it began at column 0
64
+ const model = makeModel()
65
+ expect(sweep(model, 9)).toEqual([
66
+ 'PF_INNER',
67
+ 'PF_INNER',
68
+ 'PF_INNER',
69
+ ...Array.from({ length: 8 }, () => 'PF_OUTER'),
70
+ ])
71
+ })
72
+
73
+ test('columns outside every band have no covering band', () => {
74
+ const model = makeModel()
75
+ model.setFilter('PF_OUTER', false)
76
+ const bandAt = domainBandCursor(model.domainBandsByStart.get('seq1'))
77
+ expect(bandAt(0)).toBeUndefined()
78
+ expect(bandAt(9)?.annotation.accession).toBe('PF_INNER')
79
+ expect(bandAt(19)).toBeUndefined()
80
+ })
@@ -18,8 +18,4 @@ const palettes = [
18
18
  ],
19
19
  ]
20
20
 
21
- export function getPalette(l: number) {
22
- return palettes[Math.min(l, palettes.length - 1)]!
23
- }
24
-
25
21
  export default palettes
@@ -3,7 +3,7 @@ import { describe, expect, test } from 'vitest'
3
3
  import {
4
4
  calcDepthToLeaf,
5
5
  collapse,
6
- collapsedSubtreeLengthExtent,
6
+ collapsedSubtreeMaxLength,
7
7
  find,
8
8
  findMaxBranchLen,
9
9
  hierarchy,
@@ -124,7 +124,7 @@ describe('collapse', () => {
124
124
  })
125
125
  })
126
126
 
127
- describe('collapsedSubtreeLengthExtent', () => {
127
+ describe('collapsedSubtreeMaxLength', () => {
128
128
  // root → A(1) → [ A1(2), C(3) → [ C1(1), C2(10) ] ]
129
129
  function makeLenTree(): NodeWithIds {
130
130
  return {
@@ -152,18 +152,18 @@ describe('collapsedSubtreeLengthExtent', () => {
152
152
  }
153
153
  }
154
154
 
155
- test('min/max cumulative branch length to the tips below a node', () => {
155
+ test('cumulative branch length to the farthest tip below a node', () => {
156
156
  const h = hierarchy(makeLenTree(), d => d.children)
157
157
  const nodeA = find(h, n => n.data.id === 'A')!
158
158
  collapse(nodeA)
159
- // excludes A's own branch: nearest tip A1 = 2, farthest tip C2 = 3 + 10 = 13
160
- expect(collapsedSubtreeLengthExtent(nodeA)).toEqual({ min: 2, max: 13 })
159
+ // excludes A's own branch: farthest tip C2 = 3 + 10 = 13, vs A1 = 2
160
+ expect(collapsedSubtreeMaxLength(nodeA)).toBe(13)
161
161
  })
162
162
 
163
- test('returns zero extent for a node with no descendants', () => {
163
+ test('returns zero for a node with no descendants', () => {
164
164
  const h = hierarchy(makeLenTree(), d => d.children)
165
165
  const leaf = find(h, n => n.data.id === 'A1')!
166
- expect(collapsedSubtreeLengthExtent(leaf)).toEqual({ min: 0, max: 0 })
166
+ expect(collapsedSubtreeMaxLength(leaf)).toBe(0)
167
167
  })
168
168
  })
169
169
 
package/src/hierarchy.ts CHANGED
@@ -28,9 +28,8 @@ export interface HierarchyNode<T = NodeWithIds> extends CoreHierarchyNode<T> {
28
28
  len?: number
29
29
  depthToLeaf?: number
30
30
  _children?: HierarchyNode<T>[] | null
31
- // pixel x-positions of the nearest/farthest tips of a collapsed subtree, used
32
- // to draw the collapsed-clade triangle. Set in the model's hierarchy getter.
33
- collapsedTipXNear?: number
31
+ // pixel x-position of the farthest tip of a collapsed subtree, where the base
32
+ // of the collapsed-clade triangle goes. Set in the model's hierarchy getter.
34
33
  collapsedTipXFar?: number
35
34
  }
36
35
 
@@ -115,14 +114,13 @@ export function collapse<T>(node: HierarchyNode<T>) {
115
114
  }
116
115
  }
117
116
 
118
- // Min/max cumulative raw branch length from a collapsed node down to the tips of
119
- // its detached subtree (excludes the node's own branch). Used to size the
117
+ // Cumulative raw branch length from a collapsed node down to the farthest tip of
118
+ // its detached subtree (excludes the node's own branch). Sizes the base of the
120
119
  // collapsed-clade triangle in phylogram mode.
121
- export function collapsedSubtreeLengthExtent<T extends { length?: number }>(
120
+ export function collapsedSubtreeMaxLength<T extends { length?: number }>(
122
121
  node: HierarchyNode<T>,
123
122
  ) {
124
123
  const roots = node._children ?? node.children
125
- let min = Infinity
126
124
  let max = 0
127
125
  if (roots) {
128
126
  const stack = roots.map(child => ({
@@ -140,12 +138,11 @@ export function collapsedSubtreeLengthExtent<T extends { length?: number }>(
140
138
  })
141
139
  }
142
140
  } else {
143
- min = Math.min(min, acc)
144
141
  max = Math.max(max, acc)
145
142
  }
146
143
  }
147
144
  }
148
- return { min: min === Infinity ? 0 : min, max }
145
+ return max
149
146
  }
150
147
 
151
148
  // Cladogram positioning based on ape's plot.phylo: uses topological depth (max
@@ -12,67 +12,12 @@ import { enableStaticRendering } from 'mobx-react'
12
12
  import { beforeAll, expect, test } from 'vitest'
13
13
 
14
14
  import MSAModelF from './model.ts'
15
+ import { installRenderTestEnv } from './renderTestEnv.ts'
15
16
  import { renderToSvg } from './renderToSvg.tsx'
16
17
 
17
- class Mat {
18
- constructor(
19
- public a = 1,
20
- public b = 0,
21
- public c = 0,
22
- public d = 1,
23
- public e = 0,
24
- public f = 0,
25
- ) {}
26
- multiply(o: Mat) {
27
- return new Mat(
28
- this.a * o.a + this.c * o.b,
29
- this.b * o.a + this.d * o.b,
30
- this.a * o.c + this.c * o.d,
31
- this.b * o.c + this.d * o.d,
32
- this.a * o.e + this.c * o.f + this.e,
33
- this.b * o.e + this.d * o.f + this.f,
34
- )
35
- }
36
- translate(x: number, y = 0) {
37
- return this.multiply(new Mat(1, 0, 0, 1, x, y))
38
- }
39
- scale(x: number, y = x) {
40
- return this.multiply(new Mat(x, 0, 0, y, 0, 0))
41
- }
42
- }
43
- class Pt {
44
- constructor(
45
- public x = 0,
46
- public y = 0,
47
- ) {}
48
- matrixTransform(m: Mat) {
49
- return new Pt(
50
- m.a * this.x + m.c * this.y + m.e,
51
- m.b * this.x + m.d * this.y + m.f,
52
- )
53
- }
54
- }
55
-
56
18
  beforeAll(() => {
57
19
  enableStaticRendering(true)
58
- const g = globalThis as Record<string, unknown>
59
- g.DOMMatrix = Mat
60
- g.DOMPoint = Pt
61
- HTMLCanvasElement.prototype.getContext = function () {
62
- let font = '10px sans-serif'
63
- return {
64
- get font() {
65
- return font
66
- },
67
- set font(v: string) {
68
- font = v
69
- },
70
- measureText(t: string) {
71
- const size = Number.parseFloat(font) || 10
72
- return { width: t.length * size * 0.6 } as TextMetrics
73
- },
74
- } as unknown as CanvasRenderingContext2D
75
- } as unknown as typeof HTMLCanvasElement.prototype.getContext
20
+ installRenderTestEnv()
76
21
  })
77
22
 
78
23
  // representative `impg query -o fasta-aln` block: PanSN names with region
package/src/index.ts CHANGED
@@ -11,7 +11,9 @@ export { default as MSAViewer } from './components/MSAViewer.tsx'
11
11
  export { type MsaViewModel, default as MSAModelF } from './model.ts'
12
12
  export type { MSAFormat, MSAParserType } from 'msa-parsers'
13
13
  export type { HierarchyNode } from './hierarchy.ts'
14
- export type { InterProScanResults } from './launchInterProScan.ts'
14
+ // InterProScanResults is the EBI wire format that setDomains still accepts;
15
+ // Annotation is the source-agnostic shape everything else is written against
16
+ export type { InterProScanResults } from 'msa-parsers'
15
17
  // ColumnCounts, DomainBand and TidyDomainAnnotation surface in the inferred type
16
18
  // of the composed state model, so a downstream plugin cannot emit declarations
17
19
  // for its own stateModelFactory without being able to name them (TS2883).
@@ -19,6 +21,7 @@ export type { InterProScanResults } from './launchInterProScan.ts'
19
21
  export type { ColumnCounts } from './columnCounts.ts'
20
22
  export type {
21
23
  Accession,
24
+ Annotation,
22
25
  BasicTrack,
23
26
  BasicTrackModel,
24
27
  DomainBand,
@@ -3,8 +3,8 @@ import { expect, test } from 'vitest'
3
3
 
4
4
  import MSAModelF from './model.ts'
5
5
 
6
- // jsdom has no 2d context, and measureTextCanvas throws without one. Only the
7
- // width matters here, so a stub keeps the test about tree size.
6
+ // jsdom has no 2d context, so measureTextCanvas falls back to an estimate. Only
7
+ // the width matters here, so a stub keeps the test about tree size.
8
8
  HTMLCanvasElement.prototype.getContext = (() => ({
9
9
  font: '12px sans-serif',
10
10
  measureText: (t: string) => ({ width: t.length * 7 }),
@@ -26,7 +26,42 @@ test('a tree far past the argument limit still lays out', () => {
26
26
  model.setWidth(1000)
27
27
 
28
28
  expect(model.numRows).toBe(200_000)
29
- // 's199999' is the longest name, at 7 chars * 7px
30
- expect(model.labelsWidth).toBe(49)
29
+ // 's199999' is the longest name, at 7 chars * 7px, scaled from the reference
30
+ // font size the labels are measured at down to the current one (13/16)
31
+ expect(model.labelsWidth).toBeCloseTo((49 * 13) / 16)
31
32
  expect(model.treeWidth).toBeGreaterThan(0)
32
33
  })
34
+
35
+ test('re-hovering the same tree node does not re-walk the tree', () => {
36
+ const model = MSAModelF().create({
37
+ type: 'MsaView',
38
+ data: { tree: flatNewick(200_000) },
39
+ })
40
+ model.setWidth(1000)
41
+
42
+ const leafId = model.leaves[1000]!.data.id
43
+ model.setHoveredTreeNode(leafId)
44
+ const first = model.hoveredTreeNode
45
+
46
+ // the tree's mousemove handler fires this on every event, so an unchanged id
47
+ // has to leave the state object alone: rebuilding it walks all 200k nodes and
48
+ // invalidates every overlay that reads hoveredRowIndices
49
+ model.setHoveredTreeNode(leafId)
50
+ expect(model.hoveredTreeNode).toBe(first)
51
+
52
+ model.setHoveredTreeNode(undefined)
53
+ expect(model.hoveredTreeNode).toBeUndefined()
54
+ })
55
+
56
+ test('a vertical zoom does not re-measure every label', () => {
57
+ const model = MSAModelF().create({
58
+ type: 'MsaView',
59
+ data: { tree: flatNewick(200_000) },
60
+ })
61
+ model.setWidth(1000)
62
+
63
+ const before = model.labelWidthMap
64
+ model.setRowHeight(24)
65
+ expect(model.labelWidthMap).toBe(before)
66
+ expect(model.labelsWidth).toBeCloseTo((49 * 18) / 16)
67
+ })
@@ -1,30 +1,13 @@
1
1
  import { getSession } from '@jbrowse/core/util'
2
+ import { interProScanResponseToAnnotations } from 'msa-parsers'
2
3
 
3
4
  import { isAbortError, jsonfetch, textfetch, timeout } from './fetchUtils.ts'
4
5
 
5
6
  import type { MsaViewModel } from './model.ts'
7
+ import type { InterProScanResponse } from 'msa-parsers'
6
8
 
7
9
  const base = 'https://www.ebi.ac.uk/Tools/services/rest'
8
10
 
9
- export interface InterProScanResults {
10
- matches: {
11
- signature: {
12
- entry?: {
13
- name: string
14
- description: string
15
- accession: string
16
- // original GFF feature type (exon, CDS, ...) when sourced from GFF
17
- featureType?: string
18
- }
19
- }
20
- locations: { start: number; end: number; strand?: number }[]
21
- }[]
22
- xref: { id: string }[]
23
- }
24
- export interface InterProScanResponse {
25
- results: InterProScanResults[]
26
- }
27
-
28
11
  async function runInterProScan({
29
12
  seq,
30
13
  onProgress,
@@ -169,17 +152,7 @@ async function loadInterProScanResultsWithStatus({
169
152
  url: `https://www.ebi.ac.uk/Tools/services/rest/iprscan5/result/${jobId}/json`,
170
153
  })
171
154
  const ret = await loadInterProScanResults(jobId, signal)
172
- // key each result by its xref id; skip any result lacking an xref entry
173
- // rather than crashing the whole load on one malformed result
174
- model.setDomains(
175
- Object.fromEntries(
176
- ret.results
177
- .map(r => [r.xref[0]?.id, r] as const)
178
- .filter(
179
- (e): e is [string, InterProScanResults] => e[0] !== undefined,
180
- ),
181
- ),
182
- )
155
+ model.setAnnotations(interProScanResponseToAnnotations(ret))
183
156
  getSession(model).notify(`Loaded interproscan ${jobId} results`, 'success')
184
157
  } catch (e) {
185
158
  if (isAbortError(e)) {