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
package/dist/model.js CHANGED
@@ -4,18 +4,17 @@ import { ElementId, FileLocation } from '@jbrowse/core/util/types/mst';
4
4
  import { addDisposer, cast, types } from '@jbrowse/mobx-state-tree';
5
5
  import { colord } from 'colord';
6
6
  import { autorun, transaction } from 'mobx';
7
- import { generateNodeIds, gffToInterProResults, parseEmfTree, parseGFF, parseMSA, parseNewick, } from 'msa-parsers';
7
+ import { generateNodeIds, gffToAnnotations, interProScanToAnnotations, parseEmfTree, parseGFF, parseMSA, parseNewick, } from 'msa-parsers';
8
8
  import { calculateBlocks } from './calculateBlocks.js';
9
9
  import { clustalXColumnColors } from './clustalX.js';
10
10
  import colorSchemes from './colorSchemes.js';
11
11
  import { columnCountsFromRows, letterOfResidueSlot } from './columnCounts.js';
12
- import ConservationTrack from './components/ConservationTrack.js';
13
- import TextTrack from './components/TextTrack.js';
14
- import { defaultAllowedGappyness, defaultColWidth, defaultColorSchemeName, defaultCurrentAlignment, defaultDrawMsaLetters, defaultHeight, defaultHideGaps, defaultRowHeight, defaultScrollX, defaultScrollY, defaultScrollZoom, defaultShowDomainLegend, defaultShowDomains, defaultSubFeatureRows, maxCellSize, minColWidth, minRowHeight, segmentFeatureTypes, segmentShades, } from './constants.js';
12
+ import TrackBlocks from './components/tracks/TrackBlocks.js';
13
+ import { defaultAllowedGappyness, defaultColWidth, defaultColorSchemeName, defaultCurrentAlignment, defaultDrawMsaLetters, defaultHeight, defaultHideGaps, defaultRowHeight, defaultScrollX, defaultScrollY, defaultScrollZoom, defaultShowDomainLegend, defaultShowDomains, defaultSubFeatureRows, labelReferenceFontSize, maxCellSize, minColWidth, minLetterColWidth, minLetterRowHeight, minRowHeight, segmentFeatureTypes, segmentShades, } from './constants.js';
15
14
  import { createPaletteMap } from './createPaletteMap.js';
16
15
  import { fetchTextWithProgress, isAbortError } from './fetchUtils.js';
17
16
  import { flatToTree } from './flatToTree.js';
18
- import { calcDepthToLeaf, clusterLayout, collapse, collapsedSubtreeLengthExtent, find, findMaxBranchLen, forEachDescendant, hierarchy, leaves, links, maxLength, setBrLength, sort, sum as hierarchySum, } from './hierarchy.js';
17
+ import { calcDepthToLeaf, clusterLayout, collapse, collapsedSubtreeMaxLength, find, findMaxBranchLen, forEachDescendant, hierarchy, leaves, links, maxLength, setBrLength, sort, sum as hierarchySum, } from './hierarchy.js';
19
18
  import { measureTextCanvas } from './measureTextCanvas.js';
20
19
  import { DataModelF } from './model/DataModel.js';
21
20
  import { DialogQueueSessionMixin } from './model/DialogQueue.js';
@@ -24,8 +23,9 @@ import { TreeModelF } from './model/treeModel.js';
24
23
  import { calculateNeighborJoiningTree } from './neighborJoining.js';
25
24
  import { parseAsn1 } from './parseAsn1.js';
26
25
  import { calculatePropertyConservation } from './propertyConservation.js';
27
- import { globalColToVisibleCol, visibleColToGlobalCol, visibleColToSeqPosForRow, } from './rowCoordinateCalculations.js';
26
+ import { globalColToVisibleCol, visibleColToGlobalCol, visibleColToSeqPosForRow, visibleColsBefore, } from './rowCoordinateCalculations.js';
28
27
  import { buildSeqPosIndex } from './seqPosToGlobalCol.js';
28
+ import { maxBitsFor } from './sequenceLogo.js';
29
29
  import { stripDefault } from './stripDefault.js';
30
30
  import { computeRowInsertions, len, skipBlanks, transform } from './util.js';
31
31
  import { saveAs } from './vendor/fileSaver.js';
@@ -35,11 +35,22 @@ function parseTreeText(text) {
35
35
  }
36
36
  return parseNewick(text.startsWith('SEQ') ? parseEmfTree(text).tree : text);
37
37
  }
38
+ // Tracks that start hidden. The sequence logo answers a narrower question than
39
+ // conservation does and costs three times the vertical space, so it waits to be
40
+ // asked for.
41
+ const defaultOffTracks = new Set(['sequence-logo']);
42
+ // `turnedOffTracks` records the user's explicit choices only: an id is absent
43
+ // until they touch that track, and then its value is whether the track is OFF.
44
+ // Reading the default through this is what lets a track ship hidden without
45
+ // writing an entry into every snapshot and shared URL.
46
+ function trackIsOff(turnedOffTracks, id) {
47
+ return turnedOffTracks.get(id) ?? defaultOffTracks.has(id);
48
+ }
38
49
  /**
39
50
  * #stateModel MsaView
40
51
  *
41
52
  * The main MSAView state model. Holds the loaded alignment, tree, and optional
42
- * InterProScan domain annotations, plus all display state (color scheme, zoom,
53
+ * overlay annotations, plus all display state (color scheme, zoom,
43
54
  * scroll, collapsed clades). It composes in members from `DialogQueueSessionMixin`,
44
55
  * `Tree`, and `MSAModel` (see Inherited members below). Data is loaded reactively
45
56
  * from the `msaFilehandle` / `treeFilehandle` / `gffFilehandle` properties, or set
@@ -145,7 +156,7 @@ function stateModelFactory() {
145
156
  treeMetadataFilehandle: types.maybe(FileLocation),
146
157
  /**
147
158
  * #property
148
- * filehandle object for InterProScan GFF file
159
+ * filehandle object for a GFF file of overlay annotations
149
160
  */
150
161
  gffFilehandle: types.maybe(FileLocation),
151
162
  /**
@@ -165,7 +176,10 @@ function stateModelFactory() {
165
176
  showOnly: types.maybe(types.string),
166
177
  /**
167
178
  * #property
168
- * turned off tracks
179
+ * the user's explicit show/hide choice per track id, keyed by id with
180
+ * the value meaning "off". A track the user has never touched is absent
181
+ * and falls back to its own default (see `defaultOffTracks`), so a
182
+ * hidden-by-default track adds nothing to the shared URL.
169
183
  */
170
184
  turnedOffTracks: stripDefault(types.map(types.boolean), {}),
171
185
  /**
@@ -272,6 +286,13 @@ function stateModelFactory() {
272
286
  * #volatile
273
287
  */
274
288
  conservationTrackHeight: 40,
289
+ /**
290
+ * #volatile
291
+ * taller than the conservation track by default: the logo spends its
292
+ * height on stacked glyphs, and a 40px stack of four residues leaves each
293
+ * one too short to identify
294
+ */
295
+ sequenceLogoTrackHeight: 80,
275
296
  /**
276
297
  * #volatile
277
298
  */
@@ -283,8 +304,12 @@ function stateModelFactory() {
283
304
  error: undefined,
284
305
  /**
285
306
  * #volatile
307
+ * overlay annotations drawn on the alignment, whatever their source.
308
+ * Every source -- InterProScan, GFF, a user upload -- converts to this
309
+ * flat list before it reaches the model, so nothing downstream of here
310
+ * knows which one it came from
286
311
  */
287
- interProAnnotations: undefined,
312
+ annotations: [],
288
313
  }))
289
314
  .actions(self => ({
290
315
  /**
@@ -373,7 +398,7 @@ function stateModelFactory() {
373
398
  },
374
399
  /**
375
400
  * #action
376
- * toggle the InterProScan protein-domain overlay on the alignment
401
+ * toggle the annotation overlay on the alignment
377
402
  */
378
403
  setShowDomains(arg) {
379
404
  self.showDomains = arg;
@@ -413,13 +438,6 @@ function stateModelFactory() {
413
438
  setColWidth(n) {
414
439
  self.colWidth = n;
415
440
  },
416
- /**
417
- * #action
418
- * set scroll Y-offset (px)
419
- */
420
- setScrollY(n) {
421
- self.scrollY = n;
422
- },
423
441
  /**
424
442
  * #action
425
443
  *
@@ -511,7 +529,7 @@ function stateModelFactory() {
511
529
  * #getter
512
530
  */
513
531
  get actuallyShowDomains() {
514
- return self.showDomains && !!self.interProAnnotations;
532
+ return self.showDomains && self.annotations.length > 0;
515
533
  },
516
534
  get viewInitialized() {
517
535
  return self.volatileWidth !== undefined;
@@ -562,7 +580,7 @@ function stateModelFactory() {
562
580
  return !!this.tree.noTree;
563
581
  },
564
582
  get noDomains() {
565
- return !self.interProAnnotations;
583
+ return self.annotations.length === 0;
566
584
  },
567
585
  menuItems() {
568
586
  return [];
@@ -742,12 +760,6 @@ function stateModelFactory() {
742
760
  }
743
761
  return blanks;
744
762
  },
745
- /**
746
- * #getter
747
- */
748
- get blanksSet() {
749
- return new Set(this.blanks);
750
- },
751
763
  /**
752
764
  * #getter
753
765
  * Returns a map of row name to array of insertions with display position and letters
@@ -828,13 +840,6 @@ function stateModelFactory() {
828
840
  get colStats() {
829
841
  return columnCountsFromRows(this.columns2d);
830
842
  },
831
- /**
832
- * #getter
833
- * per-column residue totals (gaps included)
834
- */
835
- get colStatsSums() {
836
- return this.colStats.totals;
837
- },
838
843
  /**
839
844
  * #getter
840
845
  * Detects sequence type based on letters present in the alignment.
@@ -893,26 +898,25 @@ function stateModelFactory() {
893
898
  * Returns values 0-1 where 1 = fully conserved, 0 = no conservation.
894
899
  */
895
900
  get conservation() {
896
- const { colStats, sequenceType } = this;
897
- const alphabetSize = sequenceType === 'amino' ? 20 : 4;
898
- const maxEntropy = Math.log2(alphabetSize);
901
+ const { colStats, alphabetMaxBits } = this;
899
902
  return Array.from({ length: colStats.numColumns }, (_, col) => {
900
903
  const total = colStats.total(col);
901
904
  const gapCount = colStats.gapCount(col);
902
- const nonGapTotal = total - gapCount;
903
- let score = 0;
904
- if (nonGapTotal > 0) {
905
- let entropy = 0;
906
- colStats.forEachResidue(col, (_slot, count) => {
907
- const freq = count / nonGapTotal;
908
- entropy -= freq * Math.log2(freq);
909
- });
910
- score =
911
- Math.max(0, 1 - entropy / maxEntropy) * (1 - gapCount / total);
912
- }
913
- return score;
905
+ return total > gapCount
906
+ ? Math.max(0, 1 - colStats.entropy(col) / alphabetMaxBits) *
907
+ (1 - gapCount / total)
908
+ : 0;
914
909
  });
915
910
  },
911
+ /**
912
+ * #getter
913
+ * The information content of a fully conserved column, in bits, which
914
+ * depends on the alphabet. Both the entropy ceiling `conservation`
915
+ * normalizes against and the y-axis ceiling of the sequence logo track.
916
+ */
917
+ get alphabetMaxBits() {
918
+ return maxBitsFor(this.sequenceType);
919
+ },
916
920
  /**
917
921
  * #getter
918
922
  * Per-column conservation of physicochemical property class (amino acids
@@ -935,14 +939,13 @@ function stateModelFactory() {
935
939
  const max = maxLength(r);
936
940
  const k = max ? self.treeWidth / max : 0;
937
941
  setBrLength(r, 0, k);
938
- // for each collapsed clade, record the pixel x-positions of its
939
- // nearest/farthest tips so the renderer can draw a triangle that spans
940
- // the branch-length extent of the hidden subtree
942
+ // for each collapsed clade, record the pixel x-position of its farthest
943
+ // tip so the renderer can draw a triangle spanning the branch-length
944
+ // extent of the hidden subtree
941
945
  forEachDescendant(r, node => {
942
946
  if (node._children) {
943
- const { min, max: mx } = collapsedSubtreeLengthExtent(node);
944
- node.collapsedTipXNear = (node.len ?? 0) + min * k;
945
- node.collapsedTipXFar = (node.len ?? 0) + mx * k;
947
+ node.collapsedTipXFar =
948
+ (node.len ?? 0) + collapsedSubtreeMaxLength(node) * k;
946
949
  }
947
950
  });
948
951
  return r;
@@ -1018,12 +1021,22 @@ function stateModelFactory() {
1018
1021
  */
1019
1022
  get blocksY() {
1020
1023
  return calculateBlocks({
1021
- viewportSize: self.height,
1024
+ viewportSize: this.visibleMsaHeight,
1022
1025
  viewportPos: -self.scrollY,
1023
1026
  blockSize: self.blockSize,
1024
1027
  mapSize: self.totalHeight,
1025
1028
  });
1026
1029
  },
1030
+ /**
1031
+ * #getter
1032
+ * height of the alignment viewport, px. The same subtraction as
1033
+ * msaAreaHeight, which is defined later in the views chain
1034
+ */
1035
+ get visibleMsaHeight() {
1036
+ return (self.height -
1037
+ self.headerHeight -
1038
+ (self.msaAreaWidth < self.totalWidth ? self.minimapHeight : 0));
1039
+ },
1027
1040
  }))
1028
1041
  .views(self => ({
1029
1042
  /**
@@ -1047,28 +1060,25 @@ function stateModelFactory() {
1047
1060
  /**
1048
1061
  * #getter
1049
1062
  * most-negative allowed scrollY, keeping the last row in view rather than
1050
- * letting the whole alignment scroll off the top. visible MSA height is
1051
- * computed inline here because msaAreaHeight is defined later in the chain.
1063
+ * letting the whole alignment scroll off the top.
1052
1064
  */
1053
1065
  get maxScrollY() {
1054
- const visibleHeight = self.height -
1055
- self.headerHeight -
1056
- (self.msaAreaWidth < self.totalWidth ? self.minimapHeight : 0);
1057
- return Math.min(-self.totalHeight + visibleHeight, 0);
1066
+ return Math.min(-self.totalHeight + self.visibleMsaHeight, 0);
1058
1067
  },
1059
1068
  /**
1060
1069
  * #getter
1061
1070
  */
1062
1071
  get showMsaLetters() {
1063
1072
  return (self.drawMsaLetters &&
1064
- self.rowHeight >= 5 &&
1073
+ self.rowHeight >= minLetterRowHeight &&
1074
+ self.colWidth >= minLetterColWidth &&
1065
1075
  self.colWidth > self.rowHeight / 2);
1066
1076
  },
1067
1077
  /**
1068
1078
  * #getter
1069
1079
  */
1070
1080
  get showTreeText() {
1071
- return self.drawLabels && self.rowHeight >= 5;
1081
+ return self.drawLabels && self.rowHeight >= minLetterRowHeight;
1072
1082
  },
1073
1083
  }))
1074
1084
  .actions(self => ({
@@ -1089,18 +1099,24 @@ function stateModelFactory() {
1089
1099
  * set hovered tree node and its descendants
1090
1100
  */
1091
1101
  setHoveredTreeNode(nodeId) {
1092
- if (nodeId) {
1093
- const node = find(self.hierarchy, n => n.data.id === nodeId);
1094
- self.hoveredTreeNode = node
1095
- ? {
1096
- nodeId,
1097
- descendantNames: leaves(node).map(leaf => leaf.data.name),
1098
- }
1099
- : undefined;
1102
+ // the tree's mousemove handler calls this on every event, and both the
1103
+ // lookup and the write are expensive: `find` walks the whole hierarchy,
1104
+ // and a fresh object here invalidates hoveredRowIndices and redraws the
1105
+ // tree and MSA overlays. Re-hovering the same node is the common case
1106
+ if (nodeId === self.hoveredTreeNode?.nodeId) {
1107
+ return;
1100
1108
  }
1101
- else {
1109
+ if (!nodeId) {
1102
1110
  self.hoveredTreeNode = undefined;
1111
+ return;
1103
1112
  }
1113
+ const node = find(self.hierarchy, n => n.data.id === nodeId);
1114
+ self.hoveredTreeNode = node
1115
+ ? {
1116
+ nodeId,
1117
+ descendantNames: leaves(node).map(leaf => leaf.data.name),
1118
+ }
1119
+ : undefined;
1104
1120
  },
1105
1121
  /**
1106
1122
  * #action
@@ -1196,21 +1212,36 @@ function stateModelFactory() {
1196
1212
  * #action
1197
1213
  */
1198
1214
  doScrollY(deltaY) {
1199
- self.scrollY = clamp(self.scrollY + deltaY, self.maxScrollY, 0);
1215
+ this.setScrollY(self.scrollY + deltaY);
1216
+ },
1217
+ /**
1218
+ * #action
1219
+ * set scroll Y-offset (px), clamped to keep the alignment in view
1220
+ */
1221
+ setScrollY(n) {
1222
+ self.scrollY = clamp(n, self.maxScrollY, 0);
1200
1223
  },
1201
1224
  /**
1202
1225
  * #action
1203
- * Set domain annotations and reveal the overlay in a single step (or clear
1204
- * both when passed undefined). Shared by every domain source: InterProScan,
1205
- * GFF, user-provided uploads, and NCBI CDD.
1226
+ * Set the overlay annotations and reveal the overlay in a single step (an
1227
+ * empty list clears both). Every source funnels through here after its
1228
+ * own adapter has flattened it: InterProScan, GFF, user uploads, NCBI CDD.
1229
+ */
1230
+ setAnnotations(annotations) {
1231
+ self.annotations = annotations;
1232
+ self.setShowDomains(annotations.length > 0);
1233
+ },
1234
+ /**
1235
+ * #action
1236
+ * set the overlay from raw InterProScan results keyed by row name. Kept
1237
+ * for downstream plugins that hold the EBI wire format; new code should
1238
+ * adapt to Annotation[] and call setAnnotations.
1206
1239
  */
1207
1240
  setDomains(data) {
1208
- self.interProAnnotations = data;
1209
- self.setShowDomains(!!data);
1241
+ this.setAnnotations(data ? interProScanToAnnotations(data) : []);
1210
1242
  },
1211
1243
  applyGFFText(gffText) {
1212
- const gffRecords = parseGFF(gffText);
1213
- this.setDomains(gffToInterProResults(gffRecords));
1244
+ this.setAnnotations(gffToAnnotations(parseGFF(gffText)));
1214
1245
  },
1215
1246
  /**
1216
1247
  * #action
@@ -1228,12 +1259,9 @@ function stateModelFactory() {
1228
1259
  * #action
1229
1260
  */
1230
1261
  toggleTrack(id) {
1231
- if (self.turnedOffTracks.has(id)) {
1232
- self.turnedOffTracks.delete(id);
1233
- }
1234
- else {
1235
- self.turnedOffTracks.set(id, true);
1236
- }
1262
+ // the stored value is "is off", so the current shown state is exactly
1263
+ // what the flipped entry should hold
1264
+ self.turnedOffTracks.set(id, !trackIsOff(self.turnedOffTracks, id));
1237
1265
  },
1238
1266
  /**
1239
1267
  * #action
@@ -1247,17 +1275,34 @@ function stateModelFactory() {
1247
1275
  * #getter
1248
1276
  */
1249
1277
  get labelWidthMap() {
1250
- const { rowHeight, leaves, treeMetadata, fontSize } = self;
1251
- return rowHeight <= 5
1252
- ? new Map()
1253
- : new Map(leaves.map(node => {
1278
+ const { showTreeText, leaves, treeMetadata } = self;
1279
+ // gated on the same condition the renderer draws labels under, so the
1280
+ // gutter labelsWidth reserves and the labels actually drawn cannot
1281
+ // disagree -- and so turning labels off hands their space to the tree.
1282
+ // Measured at a fixed reference size and scaled by labelWidthScale:
1283
+ // re-measuring every leaf on every vertical-zoom frame cost ~200ms on a
1284
+ // 50k-leaf tree
1285
+ return showTreeText
1286
+ ? new Map(leaves.map(node => {
1254
1287
  const { name } = node.data;
1255
1288
  // `||`, matching renderTreeLabels: an empty genome falls back
1256
1289
  // to the row name, and measuring '' would size the gutter (and
1257
1290
  // the label's click target) to nothing
1258
1291
  const displayName = treeMetadata[name]?.genome || name;
1259
- return [name, measureTextCanvas(displayName, fontSize)];
1260
- }));
1292
+ return [
1293
+ name,
1294
+ measureTextCanvas(displayName, labelReferenceFontSize),
1295
+ ];
1296
+ }))
1297
+ : new Map();
1298
+ },
1299
+ /**
1300
+ * #getter
1301
+ * factor turning a labelWidthMap entry into its width at the current
1302
+ * font size
1303
+ */
1304
+ get labelWidthScale() {
1305
+ return self.fontSize / labelReferenceFontSize;
1261
1306
  },
1262
1307
  get labelsWidth() {
1263
1308
  // a loop, not Math.max(...widths.values()): spreading a map of every
@@ -1271,7 +1316,7 @@ function stateModelFactory() {
1271
1316
  max = width;
1272
1317
  }
1273
1318
  }
1274
- return max;
1319
+ return max * this.labelWidthScale;
1275
1320
  },
1276
1321
  /**
1277
1322
  * #getter
@@ -1296,10 +1341,11 @@ function stateModelFactory() {
1296
1341
  .map(t => ({
1297
1342
  model: {
1298
1343
  ...t,
1344
+ kind: 'text',
1299
1345
  data: hideGapsEffective ? skipBlanks(blanks, t.data) : t.data,
1300
1346
  height: rowHeight,
1301
1347
  },
1302
- ReactComponent: TextTrack,
1348
+ ReactComponent: TrackBlocks,
1303
1349
  }));
1304
1350
  },
1305
1351
  /**
@@ -1307,34 +1353,41 @@ function stateModelFactory() {
1307
1353
  */
1308
1354
  get tracks() {
1309
1355
  const conservationTrack = {
1310
- model: {
1311
- id: 'conservation',
1312
- name: 'Conservation',
1313
- height: self.conservationTrackHeight,
1314
- barColor: 'gray',
1315
- },
1316
- ReactComponent: ConservationTrack,
1356
+ id: 'conservation',
1357
+ name: 'Conservation',
1358
+ kind: 'bar',
1359
+ height: self.conservationTrackHeight,
1360
+ barColor: 'gray',
1317
1361
  };
1318
1362
  const propertyConservationTrack = {
1319
- model: {
1320
- id: 'property-conservation',
1321
- name: 'Property conservation',
1322
- height: self.conservationTrackHeight,
1323
- barColor: '#6a51a3',
1324
- },
1325
- ReactComponent: ConservationTrack,
1363
+ id: 'property-conservation',
1364
+ name: 'Property conservation',
1365
+ kind: 'bar',
1366
+ height: self.conservationTrackHeight,
1367
+ barColor: '#6a51a3',
1368
+ };
1369
+ const sequenceLogoTrack = {
1370
+ id: 'sequence-logo',
1371
+ name: 'Sequence logo',
1372
+ kind: 'logo',
1373
+ height: self.sequenceLogoTrackHeight,
1326
1374
  };
1327
1375
  return [
1328
1376
  ...this.adapterTrackModels,
1329
- conservationTrack,
1330
- ...(self.sequenceType === 'amino' ? [propertyConservationTrack] : []),
1377
+ ...[
1378
+ conservationTrack,
1379
+ ...(self.sequenceType === 'amino'
1380
+ ? [propertyConservationTrack]
1381
+ : []),
1382
+ sequenceLogoTrack,
1383
+ ].map(model => ({ model, ReactComponent: TrackBlocks })),
1331
1384
  ];
1332
1385
  },
1333
1386
  /**
1334
1387
  * #getter
1335
1388
  */
1336
1389
  get turnedOnTracks() {
1337
- return this.tracks.filter(f => !self.turnedOffTracks.has(f.model.id));
1390
+ return this.tracks.filter(f => !trackIsOff(self.turnedOffTracks, f.model.id));
1338
1391
  },
1339
1392
  /**
1340
1393
  * #getter
@@ -1454,35 +1507,27 @@ function stateModelFactory() {
1454
1507
  get totalTrackAreaHeight() {
1455
1508
  return sum(self.turnedOnTracks.map(r => r.model.height));
1456
1509
  },
1457
- get tidyInterProAnnotationTypes() {
1458
- return new Map(this.tidyInterProAnnotations.map(annot => [annot.accession, annot]));
1459
- },
1460
- get tidyInterProAnnotations() {
1461
- return self.interProAnnotations
1462
- ? Object.entries(self.interProAnnotations)
1463
- .flatMap(([id, val]) => val.matches.flatMap(({ signature, locations }) => {
1464
- const { entry } = signature;
1465
- return entry
1466
- ? locations.map(({ start, end, strand }) => ({
1467
- id,
1468
- name: entry.name,
1469
- accession: entry.accession,
1470
- description: entry.description,
1471
- featureType: entry.featureType,
1472
- start,
1473
- end,
1474
- strand,
1475
- }))
1476
- : [];
1477
- }))
1478
- .toSorted((a, b) => len(b) - len(a))
1479
- : [];
1510
+ /**
1511
+ * one representative annotation per accession, which is what the legend,
1512
+ * the filter dialog and the palettes key off
1513
+ */
1514
+ get annotationTypes() {
1515
+ // first occurrence wins. The representative supplies only the name,
1516
+ // description and -- for ordinal segments -- the start that orders
1517
+ // them, and those agree across an accession's instances
1518
+ const types = new Map();
1519
+ for (const annot of self.annotations) {
1520
+ if (!types.has(annot.accession)) {
1521
+ types.set(annot.accession, annot);
1522
+ }
1523
+ }
1524
+ return types;
1480
1525
  },
1481
- get tidyFilteredInterProAnnotations() {
1482
- return this.tidyInterProAnnotations.filter(r => self.featureFilters.get(r.accession));
1526
+ get filteredAnnotations() {
1527
+ return self.annotations.filter(r => self.featureFilters.get(r.accession));
1483
1528
  },
1484
- get tidyFilteredGatheredInterProAnnotations() {
1485
- return groupBy(this.tidyFilteredInterProAnnotations, r => r.id);
1529
+ get annotationsByRow() {
1530
+ return groupBy(this.filteredAnnotations, r => r.id);
1486
1531
  },
1487
1532
  }))
1488
1533
  .views(self => ({
@@ -1500,6 +1545,15 @@ function stateModelFactory() {
1500
1545
  get verticalScrollbarWidth() {
1501
1546
  return self.showVerticalScrollbar ? 20 : 0;
1502
1547
  },
1548
+ /**
1549
+ * #getter
1550
+ * width of the alignment canvas itself: the msa area less the vertical
1551
+ * scrollbar sitting in it. Not usable from showHorizontalScrollbar, which
1552
+ * feeds msaAreaHeight -> showVerticalScrollbar and would close a cycle
1553
+ */
1554
+ get msaCanvasWidth() {
1555
+ return self.msaAreaWidth - this.verticalScrollbarWidth;
1556
+ },
1503
1557
  /**
1504
1558
  * #getter
1505
1559
  * ordinal segment types (exons etc.), ordered by sequence position so
@@ -1507,7 +1561,7 @@ function stateModelFactory() {
1507
1561
  * labeled by number rather than each getting a distinct hue + legend row
1508
1562
  */
1509
1563
  get segmentDomainTypes() {
1510
- return [...self.tidyInterProAnnotationTypes.values()]
1564
+ return [...self.annotationTypes.values()]
1511
1565
  .filter(d => segmentFeatureTypes.has(d.featureType ?? ''))
1512
1566
  .toSorted((a, b) => a.start - b.start);
1513
1567
  },
@@ -1517,7 +1571,7 @@ function stateModelFactory() {
1517
1571
  * their own color and a legend entry
1518
1572
  */
1519
1573
  get categoricalDomainTypes() {
1520
- return [...self.tidyInterProAnnotationTypes.values()].filter(d => !segmentFeatureTypes.has(d.featureType ?? ''));
1574
+ return [...self.annotationTypes.values()].filter(d => !segmentFeatureTypes.has(d.featureType ?? ''));
1521
1575
  },
1522
1576
  get fillPalette() {
1523
1577
  const segments = Object.fromEntries(this.segmentDomainTypes.map((d, i) => [
@@ -1558,29 +1612,39 @@ function stateModelFactory() {
1558
1612
  },
1559
1613
  /**
1560
1614
  * #getter
1561
- * every filtered-on domain annotation resolved to the visible column span
1562
- * it is drawn across, keyed by row name and in draw order (largest first,
1563
- * as tidyInterProAnnotations orders them). Resolving these once here rather
1564
- * than inside each canvas block removes a per-feature, per-block sequence
1565
- * position conversion from every redraw, and gives the letter renderer the
1566
- * band colors it needs to keep residues readable on top of the boxes.
1615
+ * every filtered-on annotation resolved to the visible column span it is
1616
+ * drawn across, keyed by row name. Each row is ordered longest-first so a
1617
+ * short domain nested inside a long one draws on top of it rather than
1618
+ * under it. Resolving these once here rather than inside each canvas
1619
+ * block removes a per-feature, per-block sequence position conversion
1620
+ * from every redraw, and gives the letter renderer the band colors it
1621
+ * needs to keep residues readable on top of the boxes.
1567
1622
  */
1568
1623
  get domainBands() {
1624
+ const { blanks } = self;
1569
1625
  const bands = new Map();
1570
- for (const [name, annotations] of Object.entries(self.tidyFilteredGatheredInterProAnnotations)) {
1626
+ for (const [name, annotations] of Object.entries(self.annotationsByRow)) {
1571
1627
  const rowBands = annotations
1572
- .map((annotation, stackIndex) => {
1573
- // InterPro positions are 1-based and inclusive; endCol is the
1574
- // exclusive column *after* the last residue, taken from that
1575
- // residue rather than from the next one, so a band stops at its own
1576
- // last residue instead of stretching across a following gap run
1577
- const startCol = self.seqPosToVisibleCol(name, annotation.start - 1);
1578
- const lastCol = self.seqPosToVisibleCol(name, annotation.end - 1);
1579
- return startCol !== undefined && lastCol !== undefined
1580
- ? { annotation, startCol, endCol: lastCol + 1, stackIndex }
1628
+ .toSorted((a, b) => len(b) - len(a))
1629
+ .map(annotation => {
1630
+ // annotation positions are 1-based and inclusive. Both ends count
1631
+ // the visible columns in front of a global column, so endCol is
1632
+ // the exclusive column after the last residue's own column --
1633
+ // the band stops there rather than stretching across a following
1634
+ // gap run -- and a residue whose column is itself hidden
1635
+ // collapses onto the neighbouring boundary instead of dropping
1636
+ // the band. A band whose every column is hidden spans nothing
1637
+ // and is left out.
1638
+ const startCol = visibleColsBefore(blanks, self.seqPosToGlobalCol(name, annotation.start - 1));
1639
+ const endCol = visibleColsBefore(blanks, self.seqPosToGlobalCol(name, annotation.end - 1) + 1);
1640
+ return endCol > startCol
1641
+ ? { annotation, startCol, endCol }
1581
1642
  : undefined;
1582
1643
  })
1583
- .filter(notEmpty);
1644
+ .filter(notEmpty)
1645
+ // numbered after the drop, so a band that resolved to nothing does
1646
+ // not leave an empty sub-row behind it
1647
+ .map((band, stackIndex) => ({ ...band, stackIndex }));
1584
1648
  if (rowBands.length > 0) {
1585
1649
  bands.set(name, rowBands);
1586
1650
  }
@@ -1669,8 +1733,11 @@ function stateModelFactory() {
1669
1733
  return undefined;
1670
1734
  }
1671
1735
  const { colStats } = self;
1736
+ if (mouseCol >= colStats.numColumns) {
1737
+ return undefined;
1738
+ }
1672
1739
  const total = colStats.total(mouseCol);
1673
- if (mouseCol >= colStats.numColumns || !total) {
1740
+ if (!total) {
1674
1741
  return undefined;
1675
1742
  }
1676
1743
  const gaps = colStats.gapCount(mouseCol);
@@ -1714,6 +1781,12 @@ function stateModelFactory() {
1714
1781
  setConservationTrackHeight(arg) {
1715
1782
  self.conservationTrackHeight = arg;
1716
1783
  },
1784
+ /**
1785
+ * #action
1786
+ */
1787
+ setSequenceLogoTrackHeight(arg) {
1788
+ self.sequenceLogoTrackHeight = arg;
1789
+ },
1717
1790
  /**
1718
1791
  * #action
1719
1792
  */
@@ -1730,7 +1803,7 @@ function stateModelFactory() {
1730
1803
  self.setTreeFilehandle(undefined);
1731
1804
  self.setMSAFilehandle(undefined);
1732
1805
  self.setGFFFilehandle(undefined);
1733
- self.setDomains(undefined);
1806
+ self.setAnnotations([]);
1734
1807
  },
1735
1808
  /**
1736
1809
  * #action
@@ -1770,7 +1843,10 @@ function stateModelFactory() {
1770
1843
  */
1771
1844
  fitHorizontally() {
1772
1845
  if (self.numColumns > 0) {
1773
- self.colWidth = clamp(self.msaAreaWidth / self.numColumns, minColWidth, maxCellSize);
1846
+ // fitting to msaAreaWidth instead left the last ~20px of columns off
1847
+ // the right edge -- and short of the width that shows a minimap, so
1848
+ // nothing on screen said they were there
1849
+ self.colWidth = clamp(self.msaCanvasWidth / self.numColumns, minColWidth, maxCellSize);
1774
1850
  }
1775
1851
  self.scrollX = 0;
1776
1852
  },
@@ -1782,7 +1858,7 @@ function stateModelFactory() {
1782
1858
  self.setHighlightedColumns(self.highlightColumns);
1783
1859
  }
1784
1860
  addDisposer(self, autorun(() => {
1785
- for (const key of self.tidyInterProAnnotationTypes.keys()) {
1861
+ for (const key of self.annotationTypes.keys()) {
1786
1862
  this.initFilter(key);
1787
1863
  }
1788
1864
  }));
@@ -1897,7 +1973,7 @@ function stateModelFactory() {
1897
1973
  }
1898
1974
  }
1899
1975
  }));
1900
- // gffFilehandle carries InterProScan domains
1976
+ // gffFilehandle carries overlay annotations
1901
1977
  loadOnFilehandleChange({
1902
1978
  getFilehandle: () => self.gffFilehandle,
1903
1979
  onLoad: text => {