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/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,
@@ -17,8 +18,7 @@ import { calculateBlocks } from './calculateBlocks.ts'
17
18
  import { clustalXColumnColors } from './clustalX.ts'
18
19
  import colorSchemes from './colorSchemes.ts'
19
20
  import { columnCountsFromRows, letterOfResidueSlot } from './columnCounts.ts'
20
- import ConservationTrack from './components/ConservationTrack.tsx'
21
- import TextTrack from './components/TextTrack.tsx'
21
+ import TrackBlocks from './components/tracks/TrackBlocks.tsx'
22
22
  import {
23
23
  defaultAllowedGappyness,
24
24
  defaultColWidth,
@@ -34,8 +34,11 @@ import {
34
34
  defaultShowDomainLegend,
35
35
  defaultShowDomains,
36
36
  defaultSubFeatureRows,
37
+ labelReferenceFontSize,
37
38
  maxCellSize,
38
39
  minColWidth,
40
+ minLetterColWidth,
41
+ minLetterRowHeight,
39
42
  minRowHeight,
40
43
  segmentFeatureTypes,
41
44
  segmentShades,
@@ -47,7 +50,7 @@ import {
47
50
  calcDepthToLeaf,
48
51
  clusterLayout,
49
52
  collapse,
50
- collapsedSubtreeLengthExtent,
53
+ collapsedSubtreeMaxLength,
51
54
  find,
52
55
  findMaxBranchLen,
53
56
  forEachDescendant,
@@ -71,15 +74,17 @@ import {
71
74
  globalColToVisibleCol,
72
75
  visibleColToGlobalCol,
73
76
  visibleColToSeqPosForRow,
77
+ visibleColsBefore,
74
78
  } from './rowCoordinateCalculations.ts'
75
79
  import { buildSeqPosIndex } from './seqPosToGlobalCol.ts'
80
+ import { maxBitsFor } from './sequenceLogo.ts'
76
81
  import { stripDefault } from './stripDefault.ts'
77
82
  import { computeRowInsertions, len, skipBlanks, transform } from './util.ts'
78
83
  import { saveAs } from './vendor/fileSaver.ts'
79
84
 
80
85
  import type { HierarchyNode } from './hierarchy.ts'
81
- import type { InterProScanResults } from './launchInterProScan.ts'
82
86
  import type {
87
+ Annotation,
83
88
  BasicTrack,
84
89
  DomainBand,
85
90
  NodeWithIds,
@@ -88,6 +93,7 @@ import type {
88
93
  import type { FileLocation as FileLocationType } from '@jbrowse/core/util/types'
89
94
  import type { Instance } from '@jbrowse/mobx-state-tree'
90
95
  import type { Theme } from '@mui/material'
96
+ import type { InterProScanResults } from 'msa-parsers'
91
97
 
92
98
  function parseTreeText(text: string) {
93
99
  if (text.startsWith('BioTreeContainer')) {
@@ -96,11 +102,27 @@ function parseTreeText(text: string) {
96
102
  return parseNewick(text.startsWith('SEQ') ? parseEmfTree(text).tree : text)
97
103
  }
98
104
 
105
+ // Tracks that start hidden. The sequence logo answers a narrower question than
106
+ // conservation does and costs three times the vertical space, so it waits to be
107
+ // asked for.
108
+ const defaultOffTracks = new Set(['sequence-logo'])
109
+
110
+ // `turnedOffTracks` records the user's explicit choices only: an id is absent
111
+ // until they touch that track, and then its value is whether the track is OFF.
112
+ // Reading the default through this is what lets a track ship hidden without
113
+ // writing an entry into every snapshot and shared URL.
114
+ function trackIsOff(
115
+ turnedOffTracks: { get: (id: string) => boolean | undefined },
116
+ id: string,
117
+ ) {
118
+ return turnedOffTracks.get(id) ?? defaultOffTracks.has(id)
119
+ }
120
+
99
121
  /**
100
122
  * #stateModel MsaView
101
123
  *
102
124
  * The main MSAView state model. Holds the loaded alignment, tree, and optional
103
- * InterProScan domain annotations, plus all display state (color scheme, zoom,
125
+ * overlay annotations, plus all display state (color scheme, zoom,
104
126
  * scroll, collapsed clades). It composes in members from `DialogQueueSessionMixin`,
105
127
  * `Tree`, and `MSAModel` (see Inherited members below). Data is loaded reactively
106
128
  * from the `msaFilehandle` / `treeFilehandle` / `gffFilehandle` properties, or set
@@ -223,7 +245,7 @@ function stateModelFactory() {
223
245
 
224
246
  /**
225
247
  * #property
226
- * filehandle object for InterProScan GFF file
248
+ * filehandle object for a GFF file of overlay annotations
227
249
  */
228
250
  gffFilehandle: types.maybe(FileLocation),
229
251
 
@@ -246,7 +268,10 @@ function stateModelFactory() {
246
268
  showOnly: types.maybe(types.string),
247
269
  /**
248
270
  * #property
249
- * turned off tracks
271
+ * the user's explicit show/hide choice per track id, keyed by id with
272
+ * the value meaning "off". A track the user has never touched is absent
273
+ * and falls back to its own default (see `defaultOffTracks`), so a
274
+ * hidden-by-default track adds nothing to the shared URL.
250
275
  */
251
276
  turnedOffTracks: stripDefault(types.map(types.boolean), {}),
252
277
 
@@ -372,6 +397,14 @@ function stateModelFactory() {
372
397
  */
373
398
  conservationTrackHeight: 40,
374
399
 
400
+ /**
401
+ * #volatile
402
+ * taller than the conservation track by default: the logo spends its
403
+ * height on stacked glyphs, and a 40px stack of four residues leaves each
404
+ * one too short to identify
405
+ */
406
+ sequenceLogoTrackHeight: 80,
407
+
375
408
  /**
376
409
  * #volatile
377
410
  */
@@ -385,10 +418,12 @@ function stateModelFactory() {
385
418
 
386
419
  /**
387
420
  * #volatile
421
+ * overlay annotations drawn on the alignment, whatever their source.
422
+ * Every source -- InterProScan, GFF, a user upload -- converts to this
423
+ * flat list before it reaches the model, so nothing downstream of here
424
+ * knows which one it came from
388
425
  */
389
- interProAnnotations: undefined as
390
- | undefined
391
- | Record<string, InterProScanResults>,
426
+ annotations: [] as Annotation[],
392
427
  }))
393
428
  .actions(self => ({
394
429
  /**
@@ -480,7 +515,7 @@ function stateModelFactory() {
480
515
  },
481
516
  /**
482
517
  * #action
483
- * toggle the InterProScan protein-domain overlay on the alignment
518
+ * toggle the annotation overlay on the alignment
484
519
  */
485
520
  setShowDomains(arg: boolean) {
486
521
  self.showDomains = arg
@@ -523,14 +558,6 @@ function stateModelFactory() {
523
558
  self.colWidth = n
524
559
  },
525
560
 
526
- /**
527
- * #action
528
- * set scroll Y-offset (px)
529
- */
530
- setScrollY(n: number) {
531
- self.scrollY = n
532
- },
533
-
534
561
  /**
535
562
  * #action
536
563
  *
@@ -638,7 +665,7 @@ function stateModelFactory() {
638
665
  * #getter
639
666
  */
640
667
  get actuallyShowDomains() {
641
- return self.showDomains && !!self.interProAnnotations
668
+ return self.showDomains && self.annotations.length > 0
642
669
  },
643
670
  get viewInitialized() {
644
671
  return self.volatileWidth !== undefined
@@ -693,7 +720,7 @@ function stateModelFactory() {
693
720
  return !!this.tree.noTree
694
721
  },
695
722
  get noDomains() {
696
- return !self.interProAnnotations
723
+ return self.annotations.length === 0
697
724
  },
698
725
  menuItems() {
699
726
  return []
@@ -883,12 +910,6 @@ function stateModelFactory() {
883
910
  }
884
911
  return blanks
885
912
  },
886
- /**
887
- * #getter
888
- */
889
- get blanksSet() {
890
- return new Set(this.blanks)
891
- },
892
913
  /**
893
914
  * #getter
894
915
  * Returns a map of row name to array of insertions with display position and letters
@@ -981,14 +1002,6 @@ function stateModelFactory() {
981
1002
  return columnCountsFromRows(this.columns2d)
982
1003
  },
983
1004
 
984
- /**
985
- * #getter
986
- * per-column residue totals (gaps included)
987
- */
988
- get colStatsSums() {
989
- return this.colStats.totals
990
- },
991
-
992
1005
  /**
993
1006
  * #getter
994
1007
  * Detects sequence type based on letters present in the alignment.
@@ -1053,27 +1066,25 @@ function stateModelFactory() {
1053
1066
  * Returns values 0-1 where 1 = fully conserved, 0 = no conservation.
1054
1067
  */
1055
1068
  get conservation() {
1056
- const { colStats, sequenceType } = this
1057
- const alphabetSize = sequenceType === 'amino' ? 20 : 4
1058
- const maxEntropy = Math.log2(alphabetSize)
1059
-
1069
+ const { colStats, alphabetMaxBits } = this
1060
1070
  return Array.from({ length: colStats.numColumns }, (_, col) => {
1061
1071
  const total = colStats.total(col)
1062
1072
  const gapCount = colStats.gapCount(col)
1063
- const nonGapTotal = total - gapCount
1064
- let score = 0
1065
- if (nonGapTotal > 0) {
1066
- let entropy = 0
1067
- colStats.forEachResidue(col, (_slot, count) => {
1068
- const freq = count / nonGapTotal
1069
- entropy -= freq * Math.log2(freq)
1070
- })
1071
- score =
1072
- Math.max(0, 1 - entropy / maxEntropy) * (1 - gapCount / total)
1073
- }
1074
- return score
1073
+ return total > gapCount
1074
+ ? Math.max(0, 1 - colStats.entropy(col) / alphabetMaxBits) *
1075
+ (1 - gapCount / total)
1076
+ : 0
1075
1077
  })
1076
1078
  },
1079
+ /**
1080
+ * #getter
1081
+ * The information content of a fully conserved column, in bits, which
1082
+ * depends on the alphabet. Both the entropy ceiling `conservation`
1083
+ * normalizes against and the y-axis ceiling of the sequence logo track.
1084
+ */
1085
+ get alphabetMaxBits() {
1086
+ return maxBitsFor(this.sequenceType)
1087
+ },
1077
1088
  /**
1078
1089
  * #getter
1079
1090
  * Per-column conservation of physicochemical property class (amino acids
@@ -1096,14 +1107,13 @@ function stateModelFactory() {
1096
1107
  const max = maxLength(r)
1097
1108
  const k = max ? self.treeWidth / max : 0
1098
1109
  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
1110
+ // for each collapsed clade, record the pixel x-position of its farthest
1111
+ // tip so the renderer can draw a triangle spanning the branch-length
1112
+ // extent of the hidden subtree
1102
1113
  forEachDescendant(r, node => {
1103
1114
  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
1115
+ node.collapsedTipXFar =
1116
+ (node.len ?? 0) + collapsedSubtreeMaxLength(node) * k
1107
1117
  }
1108
1118
  })
1109
1119
  return r as HierarchyNode<NodeWithIdsAndLength>
@@ -1186,12 +1196,24 @@ function stateModelFactory() {
1186
1196
  */
1187
1197
  get blocksY() {
1188
1198
  return calculateBlocks({
1189
- viewportSize: self.height,
1199
+ viewportSize: this.visibleMsaHeight,
1190
1200
  viewportPos: -self.scrollY,
1191
1201
  blockSize: self.blockSize,
1192
1202
  mapSize: self.totalHeight,
1193
1203
  })
1194
1204
  },
1205
+ /**
1206
+ * #getter
1207
+ * height of the alignment viewport, px. The same subtraction as
1208
+ * msaAreaHeight, which is defined later in the views chain
1209
+ */
1210
+ get visibleMsaHeight() {
1211
+ return (
1212
+ self.height -
1213
+ self.headerHeight -
1214
+ (self.msaAreaWidth < self.totalWidth ? self.minimapHeight : 0)
1215
+ )
1216
+ },
1195
1217
  }))
1196
1218
  .views(self => ({
1197
1219
  /**
@@ -1218,15 +1240,10 @@ function stateModelFactory() {
1218
1240
  /**
1219
1241
  * #getter
1220
1242
  * most-negative allowed scrollY, keeping the last row in view rather than
1221
- * letting the whole alignment scroll off the top. visible MSA height is
1222
- * computed inline here because msaAreaHeight is defined later in the chain.
1243
+ * letting the whole alignment scroll off the top.
1223
1244
  */
1224
1245
  get maxScrollY() {
1225
- const visibleHeight =
1226
- self.height -
1227
- self.headerHeight -
1228
- (self.msaAreaWidth < self.totalWidth ? self.minimapHeight : 0)
1229
- return Math.min(-self.totalHeight + visibleHeight, 0)
1246
+ return Math.min(-self.totalHeight + self.visibleMsaHeight, 0)
1230
1247
  },
1231
1248
  /**
1232
1249
  * #getter
@@ -1234,7 +1251,8 @@ function stateModelFactory() {
1234
1251
  get showMsaLetters() {
1235
1252
  return (
1236
1253
  self.drawMsaLetters &&
1237
- self.rowHeight >= 5 &&
1254
+ self.rowHeight >= minLetterRowHeight &&
1255
+ self.colWidth >= minLetterColWidth &&
1238
1256
  self.colWidth > self.rowHeight / 2
1239
1257
  )
1240
1258
  },
@@ -1242,7 +1260,7 @@ function stateModelFactory() {
1242
1260
  * #getter
1243
1261
  */
1244
1262
  get showTreeText() {
1245
- return self.drawLabels && self.rowHeight >= 5
1263
+ return self.drawLabels && self.rowHeight >= minLetterRowHeight
1246
1264
  },
1247
1265
  }))
1248
1266
  .actions(self => ({
@@ -1265,17 +1283,24 @@ function stateModelFactory() {
1265
1283
  * set hovered tree node and its descendants
1266
1284
  */
1267
1285
  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 {
1286
+ // the tree's mousemove handler calls this on every event, and both the
1287
+ // lookup and the write are expensive: `find` walks the whole hierarchy,
1288
+ // and a fresh object here invalidates hoveredRowIndices and redraws the
1289
+ // tree and MSA overlays. Re-hovering the same node is the common case
1290
+ if (nodeId === self.hoveredTreeNode?.nodeId) {
1291
+ return
1292
+ }
1293
+ if (!nodeId) {
1277
1294
  self.hoveredTreeNode = undefined
1295
+ return
1278
1296
  }
1297
+ const node = find(self.hierarchy, n => n.data.id === nodeId)
1298
+ self.hoveredTreeNode = node
1299
+ ? {
1300
+ nodeId,
1301
+ descendantNames: leaves(node).map(leaf => leaf.data.name),
1302
+ }
1303
+ : undefined
1279
1304
  },
1280
1305
 
1281
1306
  /**
@@ -1394,23 +1419,40 @@ function stateModelFactory() {
1394
1419
  * #action
1395
1420
  */
1396
1421
  doScrollY(deltaY: number) {
1397
- self.scrollY = clamp(self.scrollY + deltaY, self.maxScrollY, 0)
1422
+ this.setScrollY(self.scrollY + deltaY)
1398
1423
  },
1399
1424
 
1400
1425
  /**
1401
1426
  * #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.
1427
+ * set scroll Y-offset (px), clamped to keep the alignment in view
1428
+ */
1429
+ setScrollY(n: number) {
1430
+ self.scrollY = clamp(n, self.maxScrollY, 0)
1431
+ },
1432
+
1433
+ /**
1434
+ * #action
1435
+ * Set the overlay annotations and reveal the overlay in a single step (an
1436
+ * empty list clears both). Every source funnels through here after its
1437
+ * own adapter has flattened it: InterProScan, GFF, user uploads, NCBI CDD.
1438
+ */
1439
+ setAnnotations(annotations: Annotation[]) {
1440
+ self.annotations = annotations
1441
+ self.setShowDomains(annotations.length > 0)
1442
+ },
1443
+
1444
+ /**
1445
+ * #action
1446
+ * set the overlay from raw InterProScan results keyed by row name. Kept
1447
+ * for downstream plugins that hold the EBI wire format; new code should
1448
+ * adapt to Annotation[] and call setAnnotations.
1405
1449
  */
1406
1450
  setDomains(data?: Record<string, InterProScanResults>) {
1407
- self.interProAnnotations = data
1408
- self.setShowDomains(!!data)
1451
+ this.setAnnotations(data ? interProScanToAnnotations(data) : [])
1409
1452
  },
1410
1453
 
1411
1454
  applyGFFText(gffText: string) {
1412
- const gffRecords = parseGFF(gffText)
1413
- this.setDomains(gffToInterProResults(gffRecords))
1455
+ this.setAnnotations(gffToAnnotations(parseGFF(gffText)))
1414
1456
  },
1415
1457
 
1416
1458
  /**
@@ -1431,11 +1473,9 @@ function stateModelFactory() {
1431
1473
  * #action
1432
1474
  */
1433
1475
  toggleTrack(id: string) {
1434
- if (self.turnedOffTracks.has(id)) {
1435
- self.turnedOffTracks.delete(id)
1436
- } else {
1437
- self.turnedOffTracks.set(id, true)
1438
- }
1476
+ // the stored value is "is off", so the current shown state is exactly
1477
+ // what the flipped entry should hold
1478
+ self.turnedOffTracks.set(id, !trackIsOff(self.turnedOffTracks, id))
1439
1479
  },
1440
1480
  /**
1441
1481
  * #action
@@ -1449,19 +1489,37 @@ function stateModelFactory() {
1449
1489
  * #getter
1450
1490
  */
1451
1491
  get labelWidthMap() {
1452
- const { rowHeight, leaves, treeMetadata, fontSize } = self
1453
- return rowHeight <= 5
1454
- ? new Map<string, number>()
1455
- : new Map(
1492
+ const { showTreeText, leaves, treeMetadata } = self
1493
+ // gated on the same condition the renderer draws labels under, so the
1494
+ // gutter labelsWidth reserves and the labels actually drawn cannot
1495
+ // disagree -- and so turning labels off hands their space to the tree.
1496
+ // Measured at a fixed reference size and scaled by labelWidthScale:
1497
+ // re-measuring every leaf on every vertical-zoom frame cost ~200ms on a
1498
+ // 50k-leaf tree
1499
+ return showTreeText
1500
+ ? new Map(
1456
1501
  leaves.map(node => {
1457
1502
  const { name } = node.data
1458
1503
  // `||`, matching renderTreeLabels: an empty genome falls back
1459
1504
  // to the row name, and measuring '' would size the gutter (and
1460
1505
  // the label's click target) to nothing
1461
1506
  const displayName = treeMetadata[name]?.genome || name
1462
- return [name, measureTextCanvas(displayName, fontSize)] as const
1507
+ return [
1508
+ name,
1509
+ measureTextCanvas(displayName, labelReferenceFontSize),
1510
+ ] as const
1463
1511
  }),
1464
1512
  )
1513
+ : new Map<string, number>()
1514
+ },
1515
+
1516
+ /**
1517
+ * #getter
1518
+ * factor turning a labelWidthMap entry into its width at the current
1519
+ * font size
1520
+ */
1521
+ get labelWidthScale() {
1522
+ return self.fontSize / labelReferenceFontSize
1465
1523
  },
1466
1524
 
1467
1525
  get labelsWidth() {
@@ -1476,7 +1534,7 @@ function stateModelFactory() {
1476
1534
  max = width
1477
1535
  }
1478
1536
  }
1479
- return max
1537
+ return max * this.labelWidthScale
1480
1538
  },
1481
1539
 
1482
1540
  /**
@@ -1504,10 +1562,11 @@ function stateModelFactory() {
1504
1562
  .map(t => ({
1505
1563
  model: {
1506
1564
  ...t,
1565
+ kind: 'text' as const,
1507
1566
  data: hideGapsEffective ? skipBlanks(blanks, t.data!) : t.data,
1508
1567
  height: rowHeight,
1509
1568
  },
1510
- ReactComponent: TextTrack,
1569
+ ReactComponent: TrackBlocks,
1511
1570
  }))
1512
1571
  },
1513
1572
 
@@ -1515,28 +1574,35 @@ function stateModelFactory() {
1515
1574
  * #getter
1516
1575
  */
1517
1576
  get tracks(): BasicTrack[] {
1518
- const conservationTrack: BasicTrack = {
1519
- model: {
1520
- id: 'conservation',
1521
- name: 'Conservation',
1522
- height: self.conservationTrackHeight,
1523
- barColor: 'gray',
1524
- },
1525
- ReactComponent: ConservationTrack,
1577
+ const conservationTrack = {
1578
+ id: 'conservation',
1579
+ name: 'Conservation',
1580
+ kind: 'bar' as const,
1581
+ height: self.conservationTrackHeight,
1582
+ barColor: 'gray',
1526
1583
  }
1527
- const propertyConservationTrack: BasicTrack = {
1528
- model: {
1529
- id: 'property-conservation',
1530
- name: 'Property conservation',
1531
- height: self.conservationTrackHeight,
1532
- barColor: '#6a51a3',
1533
- },
1534
- ReactComponent: ConservationTrack,
1584
+ const propertyConservationTrack = {
1585
+ id: 'property-conservation',
1586
+ name: 'Property conservation',
1587
+ kind: 'bar' as const,
1588
+ height: self.conservationTrackHeight,
1589
+ barColor: '#6a51a3',
1590
+ }
1591
+ const sequenceLogoTrack = {
1592
+ id: 'sequence-logo',
1593
+ name: 'Sequence logo',
1594
+ kind: 'logo' as const,
1595
+ height: self.sequenceLogoTrackHeight,
1535
1596
  }
1536
1597
  return [
1537
1598
  ...this.adapterTrackModels,
1538
- conservationTrack,
1539
- ...(self.sequenceType === 'amino' ? [propertyConservationTrack] : []),
1599
+ ...[
1600
+ conservationTrack,
1601
+ ...(self.sequenceType === 'amino'
1602
+ ? [propertyConservationTrack]
1603
+ : []),
1604
+ sequenceLogoTrack,
1605
+ ].map(model => ({ model, ReactComponent: TrackBlocks })),
1540
1606
  ]
1541
1607
  },
1542
1608
 
@@ -1544,7 +1610,9 @@ function stateModelFactory() {
1544
1610
  * #getter
1545
1611
  */
1546
1612
  get turnedOnTracks() {
1547
- return this.tracks.filter(f => !self.turnedOffTracks.has(f.model.id))
1613
+ return this.tracks.filter(
1614
+ f => !trackIsOff(self.turnedOffTracks, f.model.id),
1615
+ )
1548
1616
  },
1549
1617
 
1550
1618
  /**
@@ -1674,41 +1742,29 @@ function stateModelFactory() {
1674
1742
  get totalTrackAreaHeight() {
1675
1743
  return sum(self.turnedOnTracks.map(r => r.model.height))
1676
1744
  },
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
- : []
1745
+ /**
1746
+ * one representative annotation per accession, which is what the legend,
1747
+ * the filter dialog and the palettes key off
1748
+ */
1749
+ get annotationTypes() {
1750
+ // first occurrence wins. The representative supplies only the name,
1751
+ // description and -- for ordinal segments -- the start that orders
1752
+ // them, and those agree across an accession's instances
1753
+ const types = new Map<string, Annotation>()
1754
+ for (const annot of self.annotations) {
1755
+ if (!types.has(annot.accession)) {
1756
+ types.set(annot.accession, annot)
1757
+ }
1758
+ }
1759
+ return types
1704
1760
  },
1705
- get tidyFilteredInterProAnnotations() {
1706
- return this.tidyInterProAnnotations.filter(r =>
1761
+ get filteredAnnotations() {
1762
+ return self.annotations.filter(r =>
1707
1763
  self.featureFilters.get(r.accession),
1708
1764
  )
1709
1765
  },
1710
- get tidyFilteredGatheredInterProAnnotations() {
1711
- return groupBy(this.tidyFilteredInterProAnnotations, r => r.id)
1766
+ get annotationsByRow() {
1767
+ return groupBy(this.filteredAnnotations, r => r.id)
1712
1768
  },
1713
1769
  }))
1714
1770
  .views(self => ({
@@ -1726,6 +1782,15 @@ function stateModelFactory() {
1726
1782
  get verticalScrollbarWidth() {
1727
1783
  return self.showVerticalScrollbar ? 20 : 0
1728
1784
  },
1785
+ /**
1786
+ * #getter
1787
+ * width of the alignment canvas itself: the msa area less the vertical
1788
+ * scrollbar sitting in it. Not usable from showHorizontalScrollbar, which
1789
+ * feeds msaAreaHeight -> showVerticalScrollbar and would close a cycle
1790
+ */
1791
+ get msaCanvasWidth() {
1792
+ return self.msaAreaWidth - this.verticalScrollbarWidth
1793
+ },
1729
1794
  /**
1730
1795
  * #getter
1731
1796
  * ordinal segment types (exons etc.), ordered by sequence position so
@@ -1733,7 +1798,7 @@ function stateModelFactory() {
1733
1798
  * labeled by number rather than each getting a distinct hue + legend row
1734
1799
  */
1735
1800
  get segmentDomainTypes() {
1736
- return [...self.tidyInterProAnnotationTypes.values()]
1801
+ return [...self.annotationTypes.values()]
1737
1802
  .filter(d => segmentFeatureTypes.has(d.featureType ?? ''))
1738
1803
  .toSorted((a, b) => a.start - b.start)
1739
1804
  },
@@ -1743,7 +1808,7 @@ function stateModelFactory() {
1743
1808
  * their own color and a legend entry
1744
1809
  */
1745
1810
  get categoricalDomainTypes() {
1746
- return [...self.tidyInterProAnnotationTypes.values()].filter(
1811
+ return [...self.annotationTypes.values()].filter(
1747
1812
  d => !segmentFeatureTypes.has(d.featureType ?? ''),
1748
1813
  )
1749
1814
  },
@@ -1795,34 +1860,47 @@ function stateModelFactory() {
1795
1860
 
1796
1861
  /**
1797
1862
  * #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.
1863
+ * every filtered-on annotation resolved to the visible column span it is
1864
+ * drawn across, keyed by row name. Each row is ordered longest-first so a
1865
+ * short domain nested inside a long one draws on top of it rather than
1866
+ * under it. Resolving these once here rather than inside each canvas
1867
+ * block removes a per-feature, per-block sequence position conversion
1868
+ * from every redraw, and gives the letter renderer the band colors it
1869
+ * needs to keep residues readable on top of the boxes.
1804
1870
  */
1805
1871
  get domainBands() {
1872
+ const { blanks } = self
1806
1873
  const bands = new Map<string, DomainBand[]>()
1807
1874
  for (const [name, annotations] of Object.entries(
1808
- self.tidyFilteredGatheredInterProAnnotations,
1875
+ self.annotationsByRow,
1809
1876
  )) {
1810
1877
  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,
1878
+ .toSorted((a, b) => len(b) - len(a))
1879
+ .map(annotation => {
1880
+ // annotation positions are 1-based and inclusive. Both ends count
1881
+ // the visible columns in front of a global column, so endCol is
1882
+ // the exclusive column after the last residue's own column --
1883
+ // the band stops there rather than stretching across a following
1884
+ // gap run -- and a residue whose column is itself hidden
1885
+ // collapses onto the neighbouring boundary instead of dropping
1886
+ // the band. A band whose every column is hidden spans nothing
1887
+ // and is left out.
1888
+ const startCol = visibleColsBefore(
1889
+ blanks,
1890
+ self.seqPosToGlobalCol(name, annotation.start - 1),
1819
1891
  )
1820
- const lastCol = self.seqPosToVisibleCol(name, annotation.end - 1)
1821
- return startCol !== undefined && lastCol !== undefined
1822
- ? { annotation, startCol, endCol: lastCol + 1, stackIndex }
1892
+ const endCol = visibleColsBefore(
1893
+ blanks,
1894
+ self.seqPosToGlobalCol(name, annotation.end - 1) + 1,
1895
+ )
1896
+ return endCol > startCol
1897
+ ? { annotation, startCol, endCol }
1823
1898
  : undefined
1824
1899
  })
1825
1900
  .filter(notEmpty)
1901
+ // numbered after the drop, so a band that resolved to nothing does
1902
+ // not leave an empty sub-row behind it
1903
+ .map((band, stackIndex) => ({ ...band, stackIndex }))
1826
1904
  if (rowBands.length > 0) {
1827
1905
  bands.set(name, rowBands)
1828
1906
  }
@@ -1923,8 +2001,11 @@ function stateModelFactory() {
1923
2001
  return undefined
1924
2002
  }
1925
2003
  const { colStats } = self
2004
+ if (mouseCol >= colStats.numColumns) {
2005
+ return undefined
2006
+ }
1926
2007
  const total = colStats.total(mouseCol)
1927
- if (mouseCol >= colStats.numColumns || !total) {
2008
+ if (!total) {
1928
2009
  return undefined
1929
2010
  }
1930
2011
  const gaps = colStats.gapCount(mouseCol)
@@ -1969,6 +2050,12 @@ function stateModelFactory() {
1969
2050
  setConservationTrackHeight(arg: number) {
1970
2051
  self.conservationTrackHeight = arg
1971
2052
  },
2053
+ /**
2054
+ * #action
2055
+ */
2056
+ setSequenceLogoTrackHeight(arg: number) {
2057
+ self.sequenceLogoTrackHeight = arg
2058
+ },
1972
2059
  /**
1973
2060
  * #action
1974
2061
  */
@@ -1985,7 +2072,7 @@ function stateModelFactory() {
1985
2072
  self.setTreeFilehandle(undefined)
1986
2073
  self.setMSAFilehandle(undefined)
1987
2074
  self.setGFFFilehandle(undefined)
1988
- self.setDomains(undefined)
2075
+ self.setAnnotations([])
1989
2076
  },
1990
2077
  /**
1991
2078
  * #action
@@ -2035,8 +2122,11 @@ function stateModelFactory() {
2035
2122
  */
2036
2123
  fitHorizontally() {
2037
2124
  if (self.numColumns > 0) {
2125
+ // fitting to msaAreaWidth instead left the last ~20px of columns off
2126
+ // the right edge -- and short of the width that shows a minimap, so
2127
+ // nothing on screen said they were there
2038
2128
  self.colWidth = clamp(
2039
- self.msaAreaWidth / self.numColumns,
2129
+ self.msaCanvasWidth / self.numColumns,
2040
2130
  minColWidth,
2041
2131
  maxCellSize,
2042
2132
  )
@@ -2055,7 +2145,7 @@ function stateModelFactory() {
2055
2145
  addDisposer(
2056
2146
  self,
2057
2147
  autorun(() => {
2058
- for (const key of self.tidyInterProAnnotationTypes.keys()) {
2148
+ for (const key of self.annotationTypes.keys()) {
2059
2149
  this.initFilter(key)
2060
2150
  }
2061
2151
  }),
@@ -2195,7 +2285,7 @@ function stateModelFactory() {
2195
2285
  }),
2196
2286
  )
2197
2287
 
2198
- // gffFilehandle carries InterProScan domains
2288
+ // gffFilehandle carries overlay annotations
2199
2289
  loadOnFilehandleChange({
2200
2290
  getFilehandle: () => self.gffFilehandle,
2201
2291
  onLoad: text => {