react-msaview 5.10.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 (148) 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/CopyButton.d.ts +9 -0
  6. package/dist/components/CopyButton.js +23 -0
  7. package/dist/components/CopyButton.js.map +1 -0
  8. package/dist/components/Loading.js +2 -2
  9. package/dist/components/Loading.js.map +1 -1
  10. package/dist/components/MSAView.js +2 -2
  11. package/dist/components/MSAView.js.map +1 -1
  12. package/dist/components/ResizeHandles.js +24 -40
  13. package/dist/components/ResizeHandles.js.map +1 -1
  14. package/dist/components/ScrollbarThumb.d.ts +14 -0
  15. package/dist/components/ScrollbarThumb.js +28 -0
  16. package/dist/components/ScrollbarThumb.js.map +1 -0
  17. package/dist/components/SequenceTextArea.js +3 -10
  18. package/dist/components/SequenceTextArea.js.map +1 -1
  19. package/dist/components/VerticalScrollbar.js +11 -25
  20. package/dist/components/VerticalScrollbar.js.map +1 -1
  21. package/dist/components/dialogs/FeatureDialog.js +3 -3
  22. package/dist/components/dialogs/FeatureDialog.js.map +1 -1
  23. package/dist/components/dialogs/{UserProvidedDomainsDialog.d.ts → InterProScanFileDialog.d.ts} +2 -2
  24. package/dist/components/dialogs/{UserProvidedDomainsDialog.js → InterProScanFileDialog.js} +6 -10
  25. package/dist/components/dialogs/InterProScanFileDialog.js.map +1 -0
  26. package/dist/components/dialogs/TrackInfoDialog.d.ts +2 -4
  27. package/dist/components/dialogs/TrackInfoDialog.js +8 -14
  28. package/dist/components/dialogs/TrackInfoDialog.js.map +1 -1
  29. package/dist/components/header/FileMenu.js +3 -3
  30. package/dist/components/header/FileMenu.js.map +1 -1
  31. package/dist/components/header/GappynessSlider.js +1 -1
  32. package/dist/components/header/GappynessSlider.js.map +1 -1
  33. package/dist/components/header/{getDomainsMenu.d.ts → getAnnotationsMenu.d.ts} +1 -1
  34. package/dist/components/header/{getDomainsMenu.js → getAnnotationsMenu.js} +10 -9
  35. package/dist/components/header/getAnnotationsMenu.js.map +1 -0
  36. package/dist/components/import/ImportForm.js +1 -1
  37. package/dist/components/import/ImportForm.js.map +1 -1
  38. package/dist/components/import/ImportFormExamples.js +66 -57
  39. package/dist/components/import/ImportFormExamples.js.map +1 -1
  40. package/dist/components/minimap/Minimap.js +11 -22
  41. package/dist/components/minimap/Minimap.js.map +1 -1
  42. package/dist/components/minimap/MinimapSVG.d.ts +2 -1
  43. package/dist/components/minimap/MinimapSVG.js +3 -4
  44. package/dist/components/minimap/MinimapSVG.js.map +1 -1
  45. package/dist/components/minimap/minimapLayout.d.ts +10 -1
  46. package/dist/components/minimap/minimapLayout.js +15 -10
  47. package/dist/components/minimap/minimapLayout.js.map +1 -1
  48. package/dist/components/msa/{DomainLegend.d.ts → AnnotationLegend.d.ts} +2 -2
  49. package/dist/components/msa/{DomainLegend.js → AnnotationLegend.js} +4 -4
  50. package/dist/components/msa/AnnotationLegend.js.map +1 -0
  51. package/dist/components/msa/MSACanvas.js +2 -2
  52. package/dist/components/msa/MSACanvas.js.map +1 -1
  53. package/dist/components/msa/MSAMouseoverCanvas.js +1 -2
  54. package/dist/components/msa/MSAMouseoverCanvas.js.map +1 -1
  55. package/dist/components/msa/MSAPanel.js +2 -2
  56. package/dist/components/msa/MSAPanel.js.map +1 -1
  57. package/dist/components/msa/domainBandCursor.d.ts +17 -0
  58. package/dist/components/msa/domainBandCursor.js +39 -0
  59. package/dist/components/msa/domainBandCursor.js.map +1 -0
  60. package/dist/components/msa/renderMSABlock.js +14 -20
  61. package/dist/components/msa/renderMSABlock.js.map +1 -1
  62. package/dist/components/msa/renderMSAMouseover.js +1 -2
  63. package/dist/components/msa/renderMSAMouseover.js.map +1 -1
  64. package/dist/components/tree/TreeRuler.js +5 -2
  65. package/dist/components/tree/TreeRuler.js.map +1 -1
  66. package/dist/ggplotPalettes.d.ts +0 -1
  67. package/dist/ggplotPalettes.js +0 -3
  68. package/dist/ggplotPalettes.js.map +1 -1
  69. package/dist/hierarchy.d.ts +2 -6
  70. package/dist/hierarchy.js +4 -6
  71. package/dist/hierarchy.js.map +1 -1
  72. package/dist/index.d.ts +2 -2
  73. package/dist/launchInterProScan.d.ts +0 -23
  74. package/dist/launchInterProScan.js +2 -5
  75. package/dist/launchInterProScan.js.map +1 -1
  76. package/dist/measureTextCanvas.js +21 -6
  77. package/dist/measureTextCanvas.js.map +1 -1
  78. package/dist/model.d.ts +50 -100
  79. package/dist/model.js +128 -107
  80. package/dist/model.js.map +1 -1
  81. package/dist/renderToSvg.js +2 -2
  82. package/dist/renderToSvg.js.map +1 -1
  83. package/dist/rowCoordinateCalculations.d.ts +13 -17
  84. package/dist/rowCoordinateCalculations.js +45 -39
  85. package/dist/rowCoordinateCalculations.js.map +1 -1
  86. package/dist/types.d.ts +4 -11
  87. package/dist/version.d.ts +1 -1
  88. package/dist/version.js +1 -1
  89. package/dist/version.js.map +1 -1
  90. package/package.json +5 -5
  91. package/src/clustalX.ts +22 -22
  92. package/src/colSpace.test.ts +27 -0
  93. package/src/components/CopyButton.tsx +44 -0
  94. package/src/components/Loading.tsx +10 -1
  95. package/src/components/MSAView.tsx +2 -3
  96. package/src/components/ResizeHandles.tsx +60 -69
  97. package/src/components/ScrollbarThumb.tsx +48 -0
  98. package/src/components/SequenceTextArea.tsx +3 -16
  99. package/src/components/VerticalScrollbar.tsx +19 -33
  100. package/src/components/dialogs/FeatureDialog.tsx +3 -4
  101. package/src/components/dialogs/{UserProvidedDomainsDialog.tsx → InterProScanFileDialog.tsx} +6 -14
  102. package/src/components/dialogs/TrackInfoDialog.tsx +18 -20
  103. package/src/components/header/FileMenu.tsx +3 -3
  104. package/src/components/header/GappynessSlider.tsx +4 -1
  105. package/src/components/header/{getDomainsMenu.ts → getAnnotationsMenu.ts} +10 -9
  106. package/src/components/import/ImportForm.tsx +1 -1
  107. package/src/components/import/ImportFormExamples.tsx +82 -133
  108. package/src/components/minimap/Minimap.tsx +23 -31
  109. package/src/components/minimap/MinimapSVG.tsx +32 -31
  110. package/src/components/minimap/minimapLayout.test.ts +60 -0
  111. package/src/components/minimap/minimapLayout.ts +15 -10
  112. package/src/components/msa/{DomainLegend.tsx → AnnotationLegend.tsx} +7 -3
  113. package/src/components/msa/MSACanvas.tsx +3 -10
  114. package/src/components/msa/MSAMouseoverCanvas.tsx +1 -3
  115. package/src/components/msa/MSAPanel.tsx +2 -2
  116. package/src/components/msa/domainBandCursor.ts +41 -0
  117. package/src/components/msa/renderMSABlock.ts +19 -23
  118. package/src/components/msa/renderMSAMouseover.ts +1 -3
  119. package/src/components/tree/TreeRuler.tsx +7 -2
  120. package/src/components/tree/renderTreeCanvas.test.ts +15 -7
  121. package/src/domainBands.test.ts +24 -0
  122. package/src/domainLetterColor.test.ts +80 -0
  123. package/src/ggplotPalettes.ts +0 -4
  124. package/src/hierarchy.test.ts +7 -7
  125. package/src/hierarchy.ts +6 -9
  126. package/src/index.ts +4 -1
  127. package/src/largeTree.test.ts +23 -2
  128. package/src/launchInterProScan.ts +3 -30
  129. package/src/loadErrorDisplay.test.tsx +102 -0
  130. package/src/measureTextCanvas.ts +22 -6
  131. package/src/model.ts +137 -115
  132. package/src/renderMinimapSvg.test.tsx +109 -0
  133. package/src/renderToSvg.tsx +2 -2
  134. package/src/rowCoordinateCalculations.ts +48 -44
  135. package/src/types.ts +10 -15
  136. package/src/version.ts +1 -1
  137. package/src/viewChrome.test.tsx +134 -0
  138. package/dist/components/dialogs/DomainDialog.d.ts +0 -6
  139. package/dist/components/dialogs/DomainDialog.js +0 -19
  140. package/dist/components/dialogs/DomainDialog.js.map +0 -1
  141. package/dist/components/dialogs/UserProvidedDomainsDialog.js.map +0 -1
  142. package/dist/components/header/SettingsMenu.d.ts +0 -6
  143. package/dist/components/header/SettingsMenu.js +0 -40
  144. package/dist/components/header/SettingsMenu.js.map +0 -1
  145. package/dist/components/header/getDomainsMenu.js.map +0 -1
  146. package/dist/components/msa/DomainLegend.js.map +0 -1
  147. package/src/components/dialogs/DomainDialog.tsx +0 -45
  148. package/src/components/header/SettingsMenu.tsx +0 -57
package/dist/model.js CHANGED
@@ -4,7 +4,7 @@ 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';
@@ -15,7 +15,7 @@ import { defaultAllowedGappyness, defaultColWidth, defaultColorSchemeName, defau
15
15
  import { createPaletteMap } from './createPaletteMap.js';
16
16
  import { fetchTextWithProgress, isAbortError } from './fetchUtils.js';
17
17
  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';
18
+ import { calcDepthToLeaf, clusterLayout, collapse, collapsedSubtreeMaxLength, find, findMaxBranchLen, forEachDescendant, hierarchy, leaves, links, maxLength, setBrLength, sort, sum as hierarchySum, } from './hierarchy.js';
19
19
  import { measureTextCanvas } from './measureTextCanvas.js';
20
20
  import { DataModelF } from './model/DataModel.js';
21
21
  import { DialogQueueSessionMixin } from './model/DialogQueue.js';
@@ -24,7 +24,7 @@ import { TreeModelF } from './model/treeModel.js';
24
24
  import { calculateNeighborJoiningTree } from './neighborJoining.js';
25
25
  import { parseAsn1 } from './parseAsn1.js';
26
26
  import { calculatePropertyConservation } from './propertyConservation.js';
27
- import { globalColToVisibleCol, visibleColToGlobalCol, visibleColToSeqPosForRow, } from './rowCoordinateCalculations.js';
27
+ import { globalColToVisibleCol, visibleColToGlobalCol, visibleColToSeqPosForRow, visibleColsBefore, } from './rowCoordinateCalculations.js';
28
28
  import { buildSeqPosIndex } from './seqPosToGlobalCol.js';
29
29
  import { stripDefault } from './stripDefault.js';
30
30
  import { computeRowInsertions, len, skipBlanks, transform } from './util.js';
@@ -39,7 +39,7 @@ function parseTreeText(text) {
39
39
  * #stateModel MsaView
40
40
  *
41
41
  * The main MSAView state model. Holds the loaded alignment, tree, and optional
42
- * InterProScan domain annotations, plus all display state (color scheme, zoom,
42
+ * overlay annotations, plus all display state (color scheme, zoom,
43
43
  * scroll, collapsed clades). It composes in members from `DialogQueueSessionMixin`,
44
44
  * `Tree`, and `MSAModel` (see Inherited members below). Data is loaded reactively
45
45
  * from the `msaFilehandle` / `treeFilehandle` / `gffFilehandle` properties, or set
@@ -145,7 +145,7 @@ function stateModelFactory() {
145
145
  treeMetadataFilehandle: types.maybe(FileLocation),
146
146
  /**
147
147
  * #property
148
- * filehandle object for InterProScan GFF file
148
+ * filehandle object for a GFF file of overlay annotations
149
149
  */
150
150
  gffFilehandle: types.maybe(FileLocation),
151
151
  /**
@@ -283,8 +283,12 @@ function stateModelFactory() {
283
283
  error: undefined,
284
284
  /**
285
285
  * #volatile
286
+ * overlay annotations drawn on the alignment, whatever their source.
287
+ * Every source -- InterProScan, GFF, a user upload -- converts to this
288
+ * flat list before it reaches the model, so nothing downstream of here
289
+ * knows which one it came from
286
290
  */
287
- interProAnnotations: undefined,
291
+ annotations: [],
288
292
  }))
289
293
  .actions(self => ({
290
294
  /**
@@ -373,7 +377,7 @@ function stateModelFactory() {
373
377
  },
374
378
  /**
375
379
  * #action
376
- * toggle the InterProScan protein-domain overlay on the alignment
380
+ * toggle the annotation overlay on the alignment
377
381
  */
378
382
  setShowDomains(arg) {
379
383
  self.showDomains = arg;
@@ -413,13 +417,6 @@ function stateModelFactory() {
413
417
  setColWidth(n) {
414
418
  self.colWidth = n;
415
419
  },
416
- /**
417
- * #action
418
- * set scroll Y-offset (px)
419
- */
420
- setScrollY(n) {
421
- self.scrollY = n;
422
- },
423
420
  /**
424
421
  * #action
425
422
  *
@@ -511,7 +508,7 @@ function stateModelFactory() {
511
508
  * #getter
512
509
  */
513
510
  get actuallyShowDomains() {
514
- return self.showDomains && !!self.interProAnnotations;
511
+ return self.showDomains && self.annotations.length > 0;
515
512
  },
516
513
  get viewInitialized() {
517
514
  return self.volatileWidth !== undefined;
@@ -562,7 +559,7 @@ function stateModelFactory() {
562
559
  return !!this.tree.noTree;
563
560
  },
564
561
  get noDomains() {
565
- return !self.interProAnnotations;
562
+ return self.annotations.length === 0;
566
563
  },
567
564
  menuItems() {
568
565
  return [];
@@ -742,12 +739,6 @@ function stateModelFactory() {
742
739
  }
743
740
  return blanks;
744
741
  },
745
- /**
746
- * #getter
747
- */
748
- get blanksSet() {
749
- return new Set(this.blanks);
750
- },
751
742
  /**
752
743
  * #getter
753
744
  * Returns a map of row name to array of insertions with display position and letters
@@ -828,13 +819,6 @@ function stateModelFactory() {
828
819
  get colStats() {
829
820
  return columnCountsFromRows(this.columns2d);
830
821
  },
831
- /**
832
- * #getter
833
- * per-column residue totals (gaps included)
834
- */
835
- get colStatsSums() {
836
- return this.colStats.totals;
837
- },
838
822
  /**
839
823
  * #getter
840
824
  * Detects sequence type based on letters present in the alignment.
@@ -935,14 +919,13 @@ function stateModelFactory() {
935
919
  const max = maxLength(r);
936
920
  const k = max ? self.treeWidth / max : 0;
937
921
  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
922
+ // for each collapsed clade, record the pixel x-position of its farthest
923
+ // tip so the renderer can draw a triangle spanning the branch-length
924
+ // extent of the hidden subtree
941
925
  forEachDescendant(r, node => {
942
926
  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;
927
+ node.collapsedTipXFar =
928
+ (node.len ?? 0) + collapsedSubtreeMaxLength(node) * k;
946
929
  }
947
930
  });
948
931
  return r;
@@ -1089,18 +1072,24 @@ function stateModelFactory() {
1089
1072
  * set hovered tree node and its descendants
1090
1073
  */
1091
1074
  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;
1075
+ // the tree's mousemove handler calls this on every event, and both the
1076
+ // lookup and the write are expensive: `find` walks the whole hierarchy,
1077
+ // and a fresh object here invalidates hoveredRowIndices and redraws the
1078
+ // tree and MSA overlays. Re-hovering the same node is the common case
1079
+ if (nodeId === self.hoveredTreeNode?.nodeId) {
1080
+ return;
1100
1081
  }
1101
- else {
1082
+ if (!nodeId) {
1102
1083
  self.hoveredTreeNode = undefined;
1084
+ return;
1103
1085
  }
1086
+ const node = find(self.hierarchy, n => n.data.id === nodeId);
1087
+ self.hoveredTreeNode = node
1088
+ ? {
1089
+ nodeId,
1090
+ descendantNames: leaves(node).map(leaf => leaf.data.name),
1091
+ }
1092
+ : undefined;
1104
1093
  },
1105
1094
  /**
1106
1095
  * #action
@@ -1196,21 +1185,36 @@ function stateModelFactory() {
1196
1185
  * #action
1197
1186
  */
1198
1187
  doScrollY(deltaY) {
1199
- self.scrollY = clamp(self.scrollY + deltaY, self.maxScrollY, 0);
1188
+ this.setScrollY(self.scrollY + deltaY);
1189
+ },
1190
+ /**
1191
+ * #action
1192
+ * set scroll Y-offset (px), clamped to keep the alignment in view
1193
+ */
1194
+ setScrollY(n) {
1195
+ self.scrollY = clamp(n, self.maxScrollY, 0);
1196
+ },
1197
+ /**
1198
+ * #action
1199
+ * Set the overlay annotations and reveal the overlay in a single step (an
1200
+ * empty list clears both). Every source funnels through here after its
1201
+ * own adapter has flattened it: InterProScan, GFF, user uploads, NCBI CDD.
1202
+ */
1203
+ setAnnotations(annotations) {
1204
+ self.annotations = annotations;
1205
+ self.setShowDomains(annotations.length > 0);
1200
1206
  },
1201
1207
  /**
1202
1208
  * #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.
1209
+ * set the overlay from raw InterProScan results keyed by row name. Kept
1210
+ * for downstream plugins that hold the EBI wire format; new code should
1211
+ * adapt to Annotation[] and call setAnnotations.
1206
1212
  */
1207
1213
  setDomains(data) {
1208
- self.interProAnnotations = data;
1209
- self.setShowDomains(!!data);
1214
+ this.setAnnotations(data ? interProScanToAnnotations(data) : []);
1210
1215
  },
1211
1216
  applyGFFText(gffText) {
1212
- const gffRecords = parseGFF(gffText);
1213
- this.setDomains(gffToInterProResults(gffRecords));
1217
+ this.setAnnotations(gffToAnnotations(parseGFF(gffText)));
1214
1218
  },
1215
1219
  /**
1216
1220
  * #action
@@ -1247,17 +1251,20 @@ function stateModelFactory() {
1247
1251
  * #getter
1248
1252
  */
1249
1253
  get labelWidthMap() {
1250
- const { rowHeight, leaves, treeMetadata, fontSize } = self;
1251
- return rowHeight <= 5
1252
- ? new Map()
1253
- : new Map(leaves.map(node => {
1254
+ const { showTreeText, leaves, treeMetadata, fontSize } = self;
1255
+ // gated on the same condition the renderer draws labels under, so the
1256
+ // gutter labelsWidth reserves and the labels actually drawn cannot
1257
+ // disagree -- and so turning labels off hands their space to the tree
1258
+ return showTreeText
1259
+ ? new Map(leaves.map(node => {
1254
1260
  const { name } = node.data;
1255
1261
  // `||`, matching renderTreeLabels: an empty genome falls back
1256
1262
  // to the row name, and measuring '' would size the gutter (and
1257
1263
  // the label's click target) to nothing
1258
1264
  const displayName = treeMetadata[name]?.genome || name;
1259
1265
  return [name, measureTextCanvas(displayName, fontSize)];
1260
- }));
1266
+ }))
1267
+ : new Map();
1261
1268
  },
1262
1269
  get labelsWidth() {
1263
1270
  // a loop, not Math.max(...widths.values()): spreading a map of every
@@ -1454,35 +1461,27 @@ function stateModelFactory() {
1454
1461
  get totalTrackAreaHeight() {
1455
1462
  return sum(self.turnedOnTracks.map(r => r.model.height));
1456
1463
  },
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
- : [];
1464
+ /**
1465
+ * one representative annotation per accession, which is what the legend,
1466
+ * the filter dialog and the palettes key off
1467
+ */
1468
+ get annotationTypes() {
1469
+ // first occurrence wins. The representative supplies only the name,
1470
+ // description and -- for ordinal segments -- the start that orders
1471
+ // them, and those agree across an accession's instances
1472
+ const types = new Map();
1473
+ for (const annot of self.annotations) {
1474
+ if (!types.has(annot.accession)) {
1475
+ types.set(annot.accession, annot);
1476
+ }
1477
+ }
1478
+ return types;
1480
1479
  },
1481
- get tidyFilteredInterProAnnotations() {
1482
- return this.tidyInterProAnnotations.filter(r => self.featureFilters.get(r.accession));
1480
+ get filteredAnnotations() {
1481
+ return self.annotations.filter(r => self.featureFilters.get(r.accession));
1483
1482
  },
1484
- get tidyFilteredGatheredInterProAnnotations() {
1485
- return groupBy(this.tidyFilteredInterProAnnotations, r => r.id);
1483
+ get annotationsByRow() {
1484
+ return groupBy(this.filteredAnnotations, r => r.id);
1486
1485
  },
1487
1486
  }))
1488
1487
  .views(self => ({
@@ -1500,6 +1499,15 @@ function stateModelFactory() {
1500
1499
  get verticalScrollbarWidth() {
1501
1500
  return self.showVerticalScrollbar ? 20 : 0;
1502
1501
  },
1502
+ /**
1503
+ * #getter
1504
+ * width of the alignment canvas itself: the msa area less the vertical
1505
+ * scrollbar sitting in it. Not usable from showHorizontalScrollbar, which
1506
+ * feeds msaAreaHeight -> showVerticalScrollbar and would close a cycle
1507
+ */
1508
+ get msaCanvasWidth() {
1509
+ return self.msaAreaWidth - this.verticalScrollbarWidth;
1510
+ },
1503
1511
  /**
1504
1512
  * #getter
1505
1513
  * ordinal segment types (exons etc.), ordered by sequence position so
@@ -1507,7 +1515,7 @@ function stateModelFactory() {
1507
1515
  * labeled by number rather than each getting a distinct hue + legend row
1508
1516
  */
1509
1517
  get segmentDomainTypes() {
1510
- return [...self.tidyInterProAnnotationTypes.values()]
1518
+ return [...self.annotationTypes.values()]
1511
1519
  .filter(d => segmentFeatureTypes.has(d.featureType ?? ''))
1512
1520
  .toSorted((a, b) => a.start - b.start);
1513
1521
  },
@@ -1517,7 +1525,7 @@ function stateModelFactory() {
1517
1525
  * their own color and a legend entry
1518
1526
  */
1519
1527
  get categoricalDomainTypes() {
1520
- return [...self.tidyInterProAnnotationTypes.values()].filter(d => !segmentFeatureTypes.has(d.featureType ?? ''));
1528
+ return [...self.annotationTypes.values()].filter(d => !segmentFeatureTypes.has(d.featureType ?? ''));
1521
1529
  },
1522
1530
  get fillPalette() {
1523
1531
  const segments = Object.fromEntries(this.segmentDomainTypes.map((d, i) => [
@@ -1558,29 +1566,39 @@ function stateModelFactory() {
1558
1566
  },
1559
1567
  /**
1560
1568
  * #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.
1569
+ * every filtered-on annotation resolved to the visible column span it is
1570
+ * drawn across, keyed by row name. Each row is ordered longest-first so a
1571
+ * short domain nested inside a long one draws on top of it rather than
1572
+ * under it. Resolving these once here rather than inside each canvas
1573
+ * block removes a per-feature, per-block sequence position conversion
1574
+ * from every redraw, and gives the letter renderer the band colors it
1575
+ * needs to keep residues readable on top of the boxes.
1567
1576
  */
1568
1577
  get domainBands() {
1569
1578
  const bands = new Map();
1570
- for (const [name, annotations] of Object.entries(self.tidyFilteredGatheredInterProAnnotations)) {
1579
+ for (const [name, annotations] of Object.entries(self.annotationsByRow)) {
1580
+ const { blanks } = self;
1571
1581
  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 }
1582
+ .toSorted((a, b) => len(b) - len(a))
1583
+ .map(annotation => {
1584
+ // annotation positions are 1-based and inclusive. Both ends count
1585
+ // the visible columns in front of a global column, so endCol is
1586
+ // the exclusive column after the last residue's own column --
1587
+ // the band stops there rather than stretching across a following
1588
+ // gap run -- and a residue whose column is itself hidden
1589
+ // collapses onto the neighbouring boundary instead of dropping
1590
+ // the band. A band whose every column is hidden spans nothing
1591
+ // and is left out.
1592
+ const startCol = visibleColsBefore(blanks, self.seqPosToGlobalCol(name, annotation.start - 1));
1593
+ const endCol = visibleColsBefore(blanks, self.seqPosToGlobalCol(name, annotation.end - 1) + 1);
1594
+ return endCol > startCol
1595
+ ? { annotation, startCol, endCol }
1581
1596
  : undefined;
1582
1597
  })
1583
- .filter(notEmpty);
1598
+ .filter(notEmpty)
1599
+ // numbered after the drop, so a band that resolved to nothing does
1600
+ // not leave an empty sub-row behind it
1601
+ .map((band, stackIndex) => ({ ...band, stackIndex }));
1584
1602
  if (rowBands.length > 0) {
1585
1603
  bands.set(name, rowBands);
1586
1604
  }
@@ -1730,7 +1748,7 @@ function stateModelFactory() {
1730
1748
  self.setTreeFilehandle(undefined);
1731
1749
  self.setMSAFilehandle(undefined);
1732
1750
  self.setGFFFilehandle(undefined);
1733
- self.setDomains(undefined);
1751
+ self.setAnnotations([]);
1734
1752
  },
1735
1753
  /**
1736
1754
  * #action
@@ -1770,7 +1788,10 @@ function stateModelFactory() {
1770
1788
  */
1771
1789
  fitHorizontally() {
1772
1790
  if (self.numColumns > 0) {
1773
- self.colWidth = clamp(self.msaAreaWidth / self.numColumns, minColWidth, maxCellSize);
1791
+ // fitting to msaAreaWidth instead left the last ~20px of columns off
1792
+ // the right edge -- and short of the width that shows a minimap, so
1793
+ // nothing on screen said they were there
1794
+ self.colWidth = clamp(self.msaCanvasWidth / self.numColumns, minColWidth, maxCellSize);
1774
1795
  }
1775
1796
  self.scrollX = 0;
1776
1797
  },
@@ -1782,7 +1803,7 @@ function stateModelFactory() {
1782
1803
  self.setHighlightedColumns(self.highlightColumns);
1783
1804
  }
1784
1805
  addDisposer(self, autorun(() => {
1785
- for (const key of self.tidyInterProAnnotationTypes.keys()) {
1806
+ for (const key of self.annotationTypes.keys()) {
1786
1807
  this.initFilter(key);
1787
1808
  }
1788
1809
  }));
@@ -1897,7 +1918,7 @@ function stateModelFactory() {
1897
1918
  }
1898
1919
  }
1899
1920
  }));
1900
- // gffFilehandle carries InterProScan domains
1921
+ // gffFilehandle carries overlay annotations
1901
1922
  loadOnFilehandleChange({
1902
1923
  getFilehandle: () => self.gffFilehandle,
1903
1924
  onLoad: text => {