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
package/src/model.ts CHANGED
@@ -6,7 +6,8 @@ import { colord } from 'colord'
6
6
  import { autorun, transaction } from 'mobx'
7
7
  import {
8
8
  generateNodeIds,
9
- gffToInterProResults,
9
+ gffToAnnotations,
10
+ interProScanToAnnotations,
10
11
  parseEmfTree,
11
12
  parseGFF,
12
13
  parseMSA,
@@ -47,7 +48,7 @@ import {
47
48
  calcDepthToLeaf,
48
49
  clusterLayout,
49
50
  collapse,
50
- collapsedSubtreeLengthExtent,
51
+ collapsedSubtreeMaxLength,
51
52
  find,
52
53
  findMaxBranchLen,
53
54
  forEachDescendant,
@@ -71,6 +72,7 @@ import {
71
72
  globalColToVisibleCol,
72
73
  visibleColToGlobalCol,
73
74
  visibleColToSeqPosForRow,
75
+ visibleColsBefore,
74
76
  } from './rowCoordinateCalculations.ts'
75
77
  import { buildSeqPosIndex } from './seqPosToGlobalCol.ts'
76
78
  import { stripDefault } from './stripDefault.ts'
@@ -78,8 +80,8 @@ import { computeRowInsertions, len, skipBlanks, transform } from './util.ts'
78
80
  import { saveAs } from './vendor/fileSaver.ts'
79
81
 
80
82
  import type { HierarchyNode } from './hierarchy.ts'
81
- import type { InterProScanResults } from './launchInterProScan.ts'
82
83
  import type {
84
+ Annotation,
83
85
  BasicTrack,
84
86
  DomainBand,
85
87
  NodeWithIds,
@@ -88,6 +90,7 @@ import type {
88
90
  import type { FileLocation as FileLocationType } from '@jbrowse/core/util/types'
89
91
  import type { Instance } from '@jbrowse/mobx-state-tree'
90
92
  import type { Theme } from '@mui/material'
93
+ import type { InterProScanResults } from 'msa-parsers'
91
94
 
92
95
  function parseTreeText(text: string) {
93
96
  if (text.startsWith('BioTreeContainer')) {
@@ -100,7 +103,7 @@ function parseTreeText(text: string) {
100
103
  * #stateModel MsaView
101
104
  *
102
105
  * The main MSAView state model. Holds the loaded alignment, tree, and optional
103
- * InterProScan domain annotations, plus all display state (color scheme, zoom,
106
+ * overlay annotations, plus all display state (color scheme, zoom,
104
107
  * scroll, collapsed clades). It composes in members from `DialogQueueSessionMixin`,
105
108
  * `Tree`, and `MSAModel` (see Inherited members below). Data is loaded reactively
106
109
  * from the `msaFilehandle` / `treeFilehandle` / `gffFilehandle` properties, or set
@@ -223,7 +226,7 @@ function stateModelFactory() {
223
226
 
224
227
  /**
225
228
  * #property
226
- * filehandle object for InterProScan GFF file
229
+ * filehandle object for a GFF file of overlay annotations
227
230
  */
228
231
  gffFilehandle: types.maybe(FileLocation),
229
232
 
@@ -385,10 +388,12 @@ function stateModelFactory() {
385
388
 
386
389
  /**
387
390
  * #volatile
391
+ * overlay annotations drawn on the alignment, whatever their source.
392
+ * Every source -- InterProScan, GFF, a user upload -- converts to this
393
+ * flat list before it reaches the model, so nothing downstream of here
394
+ * knows which one it came from
388
395
  */
389
- interProAnnotations: undefined as
390
- | undefined
391
- | Record<string, InterProScanResults>,
396
+ annotations: [] as Annotation[],
392
397
  }))
393
398
  .actions(self => ({
394
399
  /**
@@ -480,7 +485,7 @@ function stateModelFactory() {
480
485
  },
481
486
  /**
482
487
  * #action
483
- * toggle the InterProScan protein-domain overlay on the alignment
488
+ * toggle the annotation overlay on the alignment
484
489
  */
485
490
  setShowDomains(arg: boolean) {
486
491
  self.showDomains = arg
@@ -523,14 +528,6 @@ function stateModelFactory() {
523
528
  self.colWidth = n
524
529
  },
525
530
 
526
- /**
527
- * #action
528
- * set scroll Y-offset (px)
529
- */
530
- setScrollY(n: number) {
531
- self.scrollY = n
532
- },
533
-
534
531
  /**
535
532
  * #action
536
533
  *
@@ -638,7 +635,7 @@ function stateModelFactory() {
638
635
  * #getter
639
636
  */
640
637
  get actuallyShowDomains() {
641
- return self.showDomains && !!self.interProAnnotations
638
+ return self.showDomains && self.annotations.length > 0
642
639
  },
643
640
  get viewInitialized() {
644
641
  return self.volatileWidth !== undefined
@@ -693,7 +690,7 @@ function stateModelFactory() {
693
690
  return !!this.tree.noTree
694
691
  },
695
692
  get noDomains() {
696
- return !self.interProAnnotations
693
+ return self.annotations.length === 0
697
694
  },
698
695
  menuItems() {
699
696
  return []
@@ -883,12 +880,6 @@ function stateModelFactory() {
883
880
  }
884
881
  return blanks
885
882
  },
886
- /**
887
- * #getter
888
- */
889
- get blanksSet() {
890
- return new Set(this.blanks)
891
- },
892
883
  /**
893
884
  * #getter
894
885
  * Returns a map of row name to array of insertions with display position and letters
@@ -981,14 +972,6 @@ function stateModelFactory() {
981
972
  return columnCountsFromRows(this.columns2d)
982
973
  },
983
974
 
984
- /**
985
- * #getter
986
- * per-column residue totals (gaps included)
987
- */
988
- get colStatsSums() {
989
- return this.colStats.totals
990
- },
991
-
992
975
  /**
993
976
  * #getter
994
977
  * Detects sequence type based on letters present in the alignment.
@@ -1096,14 +1079,13 @@ function stateModelFactory() {
1096
1079
  const max = maxLength(r)
1097
1080
  const k = max ? self.treeWidth / max : 0
1098
1081
  setBrLength(r, 0, k)
1099
- // for each collapsed clade, record the pixel x-positions of its
1100
- // nearest/farthest tips so the renderer can draw a triangle that spans
1101
- // the branch-length extent of the hidden subtree
1082
+ // for each collapsed clade, record the pixel x-position of its farthest
1083
+ // tip so the renderer can draw a triangle spanning the branch-length
1084
+ // extent of the hidden subtree
1102
1085
  forEachDescendant(r, node => {
1103
1086
  if (node._children) {
1104
- const { min, max: mx } = collapsedSubtreeLengthExtent(node)
1105
- node.collapsedTipXNear = (node.len ?? 0) + min * k
1106
- node.collapsedTipXFar = (node.len ?? 0) + mx * k
1087
+ node.collapsedTipXFar =
1088
+ (node.len ?? 0) + collapsedSubtreeMaxLength(node) * k
1107
1089
  }
1108
1090
  })
1109
1091
  return r as HierarchyNode<NodeWithIdsAndLength>
@@ -1265,17 +1247,24 @@ function stateModelFactory() {
1265
1247
  * set hovered tree node and its descendants
1266
1248
  */
1267
1249
  setHoveredTreeNode(nodeId?: string) {
1268
- if (nodeId) {
1269
- const node = find(self.hierarchy, n => n.data.id === nodeId)
1270
- self.hoveredTreeNode = node
1271
- ? {
1272
- nodeId,
1273
- descendantNames: leaves(node).map(leaf => leaf.data.name),
1274
- }
1275
- : undefined
1276
- } else {
1250
+ // the tree's mousemove handler calls this on every event, and both the
1251
+ // lookup and the write are expensive: `find` walks the whole hierarchy,
1252
+ // and a fresh object here invalidates hoveredRowIndices and redraws the
1253
+ // tree and MSA overlays. Re-hovering the same node is the common case
1254
+ if (nodeId === self.hoveredTreeNode?.nodeId) {
1255
+ return
1256
+ }
1257
+ if (!nodeId) {
1277
1258
  self.hoveredTreeNode = undefined
1259
+ return
1278
1260
  }
1261
+ const node = find(self.hierarchy, n => n.data.id === nodeId)
1262
+ self.hoveredTreeNode = node
1263
+ ? {
1264
+ nodeId,
1265
+ descendantNames: leaves(node).map(leaf => leaf.data.name),
1266
+ }
1267
+ : undefined
1279
1268
  },
1280
1269
 
1281
1270
  /**
@@ -1394,23 +1383,40 @@ function stateModelFactory() {
1394
1383
  * #action
1395
1384
  */
1396
1385
  doScrollY(deltaY: number) {
1397
- self.scrollY = clamp(self.scrollY + deltaY, self.maxScrollY, 0)
1386
+ this.setScrollY(self.scrollY + deltaY)
1398
1387
  },
1399
1388
 
1400
1389
  /**
1401
1390
  * #action
1402
- * Set domain annotations and reveal the overlay in a single step (or clear
1403
- * both when passed undefined). Shared by every domain source: InterProScan,
1404
- * GFF, user-provided uploads, and NCBI CDD.
1391
+ * set scroll Y-offset (px), clamped to keep the alignment in view
1392
+ */
1393
+ setScrollY(n: number) {
1394
+ self.scrollY = clamp(n, self.maxScrollY, 0)
1395
+ },
1396
+
1397
+ /**
1398
+ * #action
1399
+ * Set the overlay annotations and reveal the overlay in a single step (an
1400
+ * empty list clears both). Every source funnels through here after its
1401
+ * own adapter has flattened it: InterProScan, GFF, user uploads, NCBI CDD.
1402
+ */
1403
+ setAnnotations(annotations: Annotation[]) {
1404
+ self.annotations = annotations
1405
+ self.setShowDomains(annotations.length > 0)
1406
+ },
1407
+
1408
+ /**
1409
+ * #action
1410
+ * set the overlay from raw InterProScan results keyed by row name. Kept
1411
+ * for downstream plugins that hold the EBI wire format; new code should
1412
+ * adapt to Annotation[] and call setAnnotations.
1405
1413
  */
1406
1414
  setDomains(data?: Record<string, InterProScanResults>) {
1407
- self.interProAnnotations = data
1408
- self.setShowDomains(!!data)
1415
+ this.setAnnotations(data ? interProScanToAnnotations(data) : [])
1409
1416
  },
1410
1417
 
1411
1418
  applyGFFText(gffText: string) {
1412
- const gffRecords = parseGFF(gffText)
1413
- this.setDomains(gffToInterProResults(gffRecords))
1419
+ this.setAnnotations(gffToAnnotations(parseGFF(gffText)))
1414
1420
  },
1415
1421
 
1416
1422
  /**
@@ -1449,10 +1455,12 @@ function stateModelFactory() {
1449
1455
  * #getter
1450
1456
  */
1451
1457
  get labelWidthMap() {
1452
- const { rowHeight, leaves, treeMetadata, fontSize } = self
1453
- return rowHeight <= 5
1454
- ? new Map<string, number>()
1455
- : new Map(
1458
+ const { showTreeText, leaves, treeMetadata, fontSize } = self
1459
+ // gated on the same condition the renderer draws labels under, so the
1460
+ // gutter labelsWidth reserves and the labels actually drawn cannot
1461
+ // disagree -- and so turning labels off hands their space to the tree
1462
+ return showTreeText
1463
+ ? new Map(
1456
1464
  leaves.map(node => {
1457
1465
  const { name } = node.data
1458
1466
  // `||`, matching renderTreeLabels: an empty genome falls back
@@ -1462,6 +1470,7 @@ function stateModelFactory() {
1462
1470
  return [name, measureTextCanvas(displayName, fontSize)] as const
1463
1471
  }),
1464
1472
  )
1473
+ : new Map<string, number>()
1465
1474
  },
1466
1475
 
1467
1476
  get labelsWidth() {
@@ -1635,8 +1644,8 @@ function stateModelFactory() {
1635
1644
  return 0
1636
1645
  }
1637
1646
  const col = self.seqPosGlobalColIndex.get(rowName)?.[seqPos]
1638
- // past the end of the ungapped sequence: 0 for the degenerate empty-row
1639
- // case, otherwise one past the last column, as the scan version returned
1647
+ // past the end of the ungapped sequence: 0 for the degenerate all-gap
1648
+ // row, otherwise one past the last column
1640
1649
  return col ?? (seqPos === 0 ? 0 : seq.length)
1641
1650
  },
1642
1651
 
@@ -1674,41 +1683,29 @@ function stateModelFactory() {
1674
1683
  get totalTrackAreaHeight() {
1675
1684
  return sum(self.turnedOnTracks.map(r => r.model.height))
1676
1685
  },
1677
- get tidyInterProAnnotationTypes() {
1678
- return new Map(
1679
- this.tidyInterProAnnotations.map(annot => [annot.accession, annot]),
1680
- )
1681
- },
1682
- get tidyInterProAnnotations() {
1683
- return self.interProAnnotations
1684
- ? Object.entries(self.interProAnnotations)
1685
- .flatMap(([id, val]) =>
1686
- val.matches.flatMap(({ signature, locations }) => {
1687
- const { entry } = signature
1688
- return entry
1689
- ? locations.map(({ start, end, strand }) => ({
1690
- id,
1691
- name: entry.name,
1692
- accession: entry.accession,
1693
- description: entry.description,
1694
- featureType: entry.featureType,
1695
- start,
1696
- end,
1697
- strand,
1698
- }))
1699
- : []
1700
- }),
1701
- )
1702
- .toSorted((a, b) => len(b) - len(a))
1703
- : []
1686
+ /**
1687
+ * one representative annotation per accession, which is what the legend,
1688
+ * the filter dialog and the palettes key off
1689
+ */
1690
+ get annotationTypes() {
1691
+ // first occurrence wins. The representative supplies only the name,
1692
+ // description and -- for ordinal segments -- the start that orders
1693
+ // them, and those agree across an accession's instances
1694
+ const types = new Map<string, Annotation>()
1695
+ for (const annot of self.annotations) {
1696
+ if (!types.has(annot.accession)) {
1697
+ types.set(annot.accession, annot)
1698
+ }
1699
+ }
1700
+ return types
1704
1701
  },
1705
- get tidyFilteredInterProAnnotations() {
1706
- return this.tidyInterProAnnotations.filter(r =>
1702
+ get filteredAnnotations() {
1703
+ return self.annotations.filter(r =>
1707
1704
  self.featureFilters.get(r.accession),
1708
1705
  )
1709
1706
  },
1710
- get tidyFilteredGatheredInterProAnnotations() {
1711
- return groupBy(this.tidyFilteredInterProAnnotations, r => r.id)
1707
+ get annotationsByRow() {
1708
+ return groupBy(this.filteredAnnotations, r => r.id)
1712
1709
  },
1713
1710
  }))
1714
1711
  .views(self => ({
@@ -1726,6 +1723,15 @@ function stateModelFactory() {
1726
1723
  get verticalScrollbarWidth() {
1727
1724
  return self.showVerticalScrollbar ? 20 : 0
1728
1725
  },
1726
+ /**
1727
+ * #getter
1728
+ * width of the alignment canvas itself: the msa area less the vertical
1729
+ * scrollbar sitting in it. Not usable from showHorizontalScrollbar, which
1730
+ * feeds msaAreaHeight -> showVerticalScrollbar and would close a cycle
1731
+ */
1732
+ get msaCanvasWidth() {
1733
+ return self.msaAreaWidth - this.verticalScrollbarWidth
1734
+ },
1729
1735
  /**
1730
1736
  * #getter
1731
1737
  * ordinal segment types (exons etc.), ordered by sequence position so
@@ -1733,7 +1739,7 @@ function stateModelFactory() {
1733
1739
  * labeled by number rather than each getting a distinct hue + legend row
1734
1740
  */
1735
1741
  get segmentDomainTypes() {
1736
- return [...self.tidyInterProAnnotationTypes.values()]
1742
+ return [...self.annotationTypes.values()]
1737
1743
  .filter(d => segmentFeatureTypes.has(d.featureType ?? ''))
1738
1744
  .toSorted((a, b) => a.start - b.start)
1739
1745
  },
@@ -1743,7 +1749,7 @@ function stateModelFactory() {
1743
1749
  * their own color and a legend entry
1744
1750
  */
1745
1751
  get categoricalDomainTypes() {
1746
- return [...self.tidyInterProAnnotationTypes.values()].filter(
1752
+ return [...self.annotationTypes.values()].filter(
1747
1753
  d => !segmentFeatureTypes.has(d.featureType ?? ''),
1748
1754
  )
1749
1755
  },
@@ -1795,34 +1801,47 @@ function stateModelFactory() {
1795
1801
 
1796
1802
  /**
1797
1803
  * #getter
1798
- * every filtered-on domain annotation resolved to the visible column span
1799
- * it is drawn across, keyed by row name and in draw order (largest first,
1800
- * as tidyInterProAnnotations orders them). Resolving these once here rather
1801
- * than inside each canvas block removes a per-feature, per-block sequence
1802
- * position conversion from every redraw, and gives the letter renderer the
1803
- * band colors it needs to keep residues readable on top of the boxes.
1804
+ * every filtered-on annotation resolved to the visible column span it is
1805
+ * drawn across, keyed by row name. Each row is ordered longest-first so a
1806
+ * short domain nested inside a long one draws on top of it rather than
1807
+ * under it. Resolving these once here rather than inside each canvas
1808
+ * block removes a per-feature, per-block sequence position conversion
1809
+ * from every redraw, and gives the letter renderer the band colors it
1810
+ * needs to keep residues readable on top of the boxes.
1804
1811
  */
1805
1812
  get domainBands() {
1806
1813
  const bands = new Map<string, DomainBand[]>()
1807
1814
  for (const [name, annotations] of Object.entries(
1808
- self.tidyFilteredGatheredInterProAnnotations,
1815
+ self.annotationsByRow,
1809
1816
  )) {
1817
+ const { blanks } = self
1810
1818
  const rowBands = annotations
1811
- .map((annotation, stackIndex) => {
1812
- // InterPro positions are 1-based and inclusive; endCol is the
1813
- // exclusive column *after* the last residue, taken from that
1814
- // residue rather than from the next one, so a band stops at its own
1815
- // last residue instead of stretching across a following gap run
1816
- const startCol = self.seqPosToVisibleCol(
1817
- name,
1818
- annotation.start - 1,
1819
+ .toSorted((a, b) => len(b) - len(a))
1820
+ .map(annotation => {
1821
+ // annotation positions are 1-based and inclusive. Both ends count
1822
+ // the visible columns in front of a global column, so endCol is
1823
+ // the exclusive column after the last residue's own column --
1824
+ // the band stops there rather than stretching across a following
1825
+ // gap run -- and a residue whose column is itself hidden
1826
+ // collapses onto the neighbouring boundary instead of dropping
1827
+ // the band. A band whose every column is hidden spans nothing
1828
+ // and is left out.
1829
+ const startCol = visibleColsBefore(
1830
+ blanks,
1831
+ self.seqPosToGlobalCol(name, annotation.start - 1),
1832
+ )
1833
+ const endCol = visibleColsBefore(
1834
+ blanks,
1835
+ self.seqPosToGlobalCol(name, annotation.end - 1) + 1,
1819
1836
  )
1820
- const lastCol = self.seqPosToVisibleCol(name, annotation.end - 1)
1821
- return startCol !== undefined && lastCol !== undefined
1822
- ? { annotation, startCol, endCol: lastCol + 1, stackIndex }
1837
+ return endCol > startCol
1838
+ ? { annotation, startCol, endCol }
1823
1839
  : undefined
1824
1840
  })
1825
1841
  .filter(notEmpty)
1842
+ // numbered after the drop, so a band that resolved to nothing does
1843
+ // not leave an empty sub-row behind it
1844
+ .map((band, stackIndex) => ({ ...band, stackIndex }))
1826
1845
  if (rowBands.length > 0) {
1827
1846
  bands.set(name, rowBands)
1828
1847
  }
@@ -1985,7 +2004,7 @@ function stateModelFactory() {
1985
2004
  self.setTreeFilehandle(undefined)
1986
2005
  self.setMSAFilehandle(undefined)
1987
2006
  self.setGFFFilehandle(undefined)
1988
- self.setDomains(undefined)
2007
+ self.setAnnotations([])
1989
2008
  },
1990
2009
  /**
1991
2010
  * #action
@@ -2035,8 +2054,11 @@ function stateModelFactory() {
2035
2054
  */
2036
2055
  fitHorizontally() {
2037
2056
  if (self.numColumns > 0) {
2057
+ // fitting to msaAreaWidth instead left the last ~20px of columns off
2058
+ // the right edge -- and short of the width that shows a minimap, so
2059
+ // nothing on screen said they were there
2038
2060
  self.colWidth = clamp(
2039
- self.msaAreaWidth / self.numColumns,
2061
+ self.msaCanvasWidth / self.numColumns,
2040
2062
  minColWidth,
2041
2063
  maxCellSize,
2042
2064
  )
@@ -2055,7 +2077,7 @@ function stateModelFactory() {
2055
2077
  addDisposer(
2056
2078
  self,
2057
2079
  autorun(() => {
2058
- for (const key of self.tidyInterProAnnotationTypes.keys()) {
2080
+ for (const key of self.annotationTypes.keys()) {
2059
2081
  this.initFilter(key)
2060
2082
  }
2061
2083
  }),
@@ -2084,79 +2106,100 @@ function stateModelFactory() {
2084
2106
  })
2085
2107
  }
2086
2108
 
2087
- // autorun opens treeFilehandle. generation guard: if the filehandle
2088
- // changes mid-fetch, a slower earlier request must not clobber the data
2089
- // (or loading flag) set by the newer one
2090
- let treeGeneration = 0
2091
- addDisposer(
2092
- self,
2093
- autorun(async () => {
2094
- const { treeFilehandle } = self
2095
- if (treeFilehandle) {
2096
- const generation = ++treeGeneration
2109
+ /**
2110
+ * Fetch a filehandle whenever it changes, and hand the text to
2111
+ * `onLoad`.
2112
+ *
2113
+ * Every loader carries a generation guard: the filehandle can change
2114
+ * mid-fetch (a second file picked while the first is still in flight),
2115
+ * and the slower earlier request must not clobber the data, status, or
2116
+ * loading flag belonging to the newer one.
2117
+ *
2118
+ * `clearFilehandle` serves two purposes for the loaders that pass it.
2119
+ * A local file has no URL to refetch from, so the handle is dropped
2120
+ * once its bytes are in the model; and a fetch the user cancels drops
2121
+ * it too, returning the view to the import form rather than leaving a
2122
+ * stuck spinner.
2123
+ */
2124
+ const loadOnFilehandleChange = ({
2125
+ getFilehandle,
2126
+ onLoad,
2127
+ setLoading,
2128
+ clearFilehandle,
2129
+ }: {
2130
+ getFilehandle: () => FileLocationType | undefined
2131
+ onLoad: (text: string) => void
2132
+ setLoading?: (arg: boolean) => void
2133
+ clearFilehandle?: () => void
2134
+ }) => {
2135
+ let generation = 0
2136
+ addDisposer(
2137
+ self,
2138
+ autorun(async () => {
2139
+ const filehandle = getFilehandle()
2140
+ if (!filehandle) {
2141
+ return
2142
+ }
2143
+ const current = ++generation
2144
+ const isCurrent = () => current === generation
2097
2145
  try {
2098
- self.setLoadingTree(true)
2146
+ setLoading?.(true)
2147
+ self.setError(undefined)
2099
2148
  const text = await fetchTextWithProgress(
2100
- openLocation(treeFilehandle),
2149
+ openLocation(filehandle),
2101
2150
  status => {
2102
- if (generation === treeGeneration) {
2151
+ if (isCurrent()) {
2103
2152
  self.setStatus(status)
2104
2153
  }
2105
2154
  },
2106
2155
  )
2107
- if (generation === treeGeneration) {
2156
+ if (isCurrent()) {
2108
2157
  transaction(() => {
2109
- self.setTree(text)
2110
- if (treeFilehandle.locationType === 'BlobLocation') {
2111
- // clear filehandle after loading if from a local file
2112
- self.setTreeFilehandle(undefined)
2158
+ onLoad(text)
2159
+ if (filehandle.locationType === 'BlobLocation') {
2160
+ clearFilehandle?.()
2113
2161
  }
2114
2162
  })
2115
2163
  }
2116
2164
  } catch (e) {
2117
- if (generation === treeGeneration) {
2165
+ if (isCurrent()) {
2118
2166
  if (isAbortError(e)) {
2119
- // cancelled by the user: drop the filehandle so the view
2120
- // returns to the import form instead of a stuck spinner
2121
- self.setTreeFilehandle(undefined)
2167
+ clearFilehandle?.()
2122
2168
  } else {
2123
2169
  console.error(e)
2124
2170
  self.setError(e)
2125
2171
  }
2126
2172
  }
2127
2173
  } finally {
2128
- if (generation === treeGeneration) {
2129
- self.setLoadingTree(false)
2130
- }
2131
- }
2132
- }
2133
- }),
2134
- )
2135
- // autorun opens treeMetadataFilehandle
2136
- addDisposer(
2137
- self,
2138
- autorun(async () => {
2139
- const { treeMetadataFilehandle } = self
2140
- if (treeMetadataFilehandle) {
2141
- try {
2142
- self.setTreeMetadata(
2143
- await fetchTextWithProgress(
2144
- openLocation(treeMetadataFilehandle),
2145
- status => {
2146
- self.setStatus(status)
2147
- },
2148
- ),
2149
- )
2150
- } catch (e) {
2151
- // ignore user cancel (isAbortError); treeMetadata is optional
2152
- if (!isAbortError(e)) {
2153
- console.error(e)
2154
- self.setError(e)
2174
+ if (isCurrent()) {
2175
+ setLoading?.(false)
2155
2176
  }
2156
2177
  }
2157
- }
2158
- }),
2159
- )
2178
+ }),
2179
+ )
2180
+ }
2181
+
2182
+ loadOnFilehandleChange({
2183
+ getFilehandle: () => self.treeFilehandle,
2184
+ onLoad: text => {
2185
+ self.setTree(text)
2186
+ },
2187
+ setLoading: arg => {
2188
+ self.setLoadingTree(arg)
2189
+ },
2190
+ clearFilehandle: () => {
2191
+ self.setTreeFilehandle(undefined)
2192
+ },
2193
+ })
2194
+
2195
+ // treeMetadata is decorative and has no import-form step of its own, so
2196
+ // it keeps no loading flag and nothing to return to on cancel
2197
+ loadOnFilehandleChange({
2198
+ getFilehandle: () => self.treeMetadataFilehandle,
2199
+ onLoad: text => {
2200
+ self.setTreeMetadata(text)
2201
+ },
2202
+ })
2160
2203
 
2161
2204
  // autorun parses inline gff text from data.gff
2162
2205
  addDisposer(
@@ -2174,83 +2217,29 @@ function stateModelFactory() {
2174
2217
  }),
2175
2218
  )
2176
2219
 
2177
- // autorun opens gffFilehandle for InterProScan domains
2178
- addDisposer(
2179
- self,
2180
- autorun(async () => {
2181
- const { gffFilehandle } = self
2182
- if (gffFilehandle) {
2183
- try {
2184
- const gffText = await fetchTextWithProgress(
2185
- openLocation(gffFilehandle),
2186
- status => {
2187
- self.setStatus(status)
2188
- },
2189
- )
2190
- self.applyGFFText(gffText)
2191
- if (gffFilehandle.locationType === 'BlobLocation') {
2192
- self.setGFFFilehandle(undefined)
2193
- }
2194
- } catch (e) {
2195
- // on user cancel (isAbortError) drop the filehandle
2196
- if (isAbortError(e)) {
2197
- self.setGFFFilehandle(undefined)
2198
- } else {
2199
- console.error(e)
2200
- self.setError(e)
2201
- }
2202
- }
2203
- }
2204
- }),
2205
- )
2220
+ // gffFilehandle carries overlay annotations
2221
+ loadOnFilehandleChange({
2222
+ getFilehandle: () => self.gffFilehandle,
2223
+ onLoad: text => {
2224
+ self.applyGFFText(text)
2225
+ },
2226
+ clearFilehandle: () => {
2227
+ self.setGFFFilehandle(undefined)
2228
+ },
2229
+ })
2206
2230
 
2207
- // autorun opens msaFilehandle. generation guard: see treeFilehandle
2208
- let msaGeneration = 0
2209
- addDisposer(
2210
- self,
2211
- autorun(async () => {
2212
- const { msaFilehandle } = self
2213
- if (msaFilehandle) {
2214
- const generation = ++msaGeneration
2215
- try {
2216
- self.setLoadingMSA(true)
2217
- self.setError(undefined)
2218
- const txt = await fetchTextWithProgress(
2219
- openLocation(msaFilehandle),
2220
- status => {
2221
- if (generation === msaGeneration) {
2222
- self.setStatus(status)
2223
- }
2224
- },
2225
- )
2226
- if (generation === msaGeneration) {
2227
- transaction(() => {
2228
- self.setMSA(txt)
2229
- if (msaFilehandle.locationType === 'BlobLocation') {
2230
- // clear filehandle after loading if from a local file
2231
- self.setMSAFilehandle(undefined)
2232
- }
2233
- })
2234
- }
2235
- } catch (e) {
2236
- if (generation === msaGeneration) {
2237
- if (isAbortError(e)) {
2238
- // cancelled by the user: drop the filehandle so the view
2239
- // returns to the import form instead of a stuck spinner
2240
- self.setMSAFilehandle(undefined)
2241
- } else {
2242
- console.error(e)
2243
- self.setError(e)
2244
- }
2245
- }
2246
- } finally {
2247
- if (generation === msaGeneration) {
2248
- self.setLoadingMSA(false)
2249
- }
2250
- }
2251
- }
2252
- }),
2253
- )
2231
+ loadOnFilehandleChange({
2232
+ getFilehandle: () => self.msaFilehandle,
2233
+ onLoad: text => {
2234
+ self.setMSA(text)
2235
+ },
2236
+ setLoading: arg => {
2237
+ self.setLoadingMSA(arg)
2238
+ },
2239
+ clearFilehandle: () => {
2240
+ self.setMSAFilehandle(undefined)
2241
+ },
2242
+ })
2254
2243
 
2255
2244
  // Keep the parse chain warm: reading self.columns transitively holds
2256
2245
  // self.MSA (parseMSA) computed alive, so it is parsed once per data