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/src/model.ts CHANGED
@@ -6,7 +6,8 @@ import { colord } from 'colord'
6
6
  import { autorun, transaction } from 'mobx'
7
7
  import {
8
8
  generateNodeIds,
9
- gffToInterProResults,
9
+ gffToAnnotations,
10
+ interProScanToAnnotations,
10
11
  parseEmfTree,
11
12
  parseGFF,
12
13
  parseMSA,
@@ -47,7 +48,7 @@ import {
47
48
  calcDepthToLeaf,
48
49
  clusterLayout,
49
50
  collapse,
50
- collapsedSubtreeLengthExtent,
51
+ collapsedSubtreeMaxLength,
51
52
  find,
52
53
  findMaxBranchLen,
53
54
  forEachDescendant,
@@ -71,6 +72,7 @@ import {
71
72
  globalColToVisibleCol,
72
73
  visibleColToGlobalCol,
73
74
  visibleColToSeqPosForRow,
75
+ visibleColsBefore,
74
76
  } from './rowCoordinateCalculations.ts'
75
77
  import { buildSeqPosIndex } from './seqPosToGlobalCol.ts'
76
78
  import { stripDefault } from './stripDefault.ts'
@@ -78,8 +80,8 @@ import { computeRowInsertions, len, skipBlanks, transform } from './util.ts'
78
80
  import { saveAs } from './vendor/fileSaver.ts'
79
81
 
80
82
  import type { HierarchyNode } from './hierarchy.ts'
81
- import type { InterProScanResults } from './launchInterProScan.ts'
82
83
  import type {
84
+ Annotation,
83
85
  BasicTrack,
84
86
  DomainBand,
85
87
  NodeWithIds,
@@ -88,6 +90,7 @@ import type {
88
90
  import type { FileLocation as FileLocationType } from '@jbrowse/core/util/types'
89
91
  import type { Instance } from '@jbrowse/mobx-state-tree'
90
92
  import type { Theme } from '@mui/material'
93
+ import type { InterProScanResults } from 'msa-parsers'
91
94
 
92
95
  function parseTreeText(text: string) {
93
96
  if (text.startsWith('BioTreeContainer')) {
@@ -100,7 +103,7 @@ function parseTreeText(text: string) {
100
103
  * #stateModel MsaView
101
104
  *
102
105
  * The main MSAView state model. Holds the loaded alignment, tree, and optional
103
- * InterProScan domain annotations, plus all display state (color scheme, zoom,
106
+ * overlay annotations, plus all display state (color scheme, zoom,
104
107
  * scroll, collapsed clades). It composes in members from `DialogQueueSessionMixin`,
105
108
  * `Tree`, and `MSAModel` (see Inherited members below). Data is loaded reactively
106
109
  * from the `msaFilehandle` / `treeFilehandle` / `gffFilehandle` properties, or set
@@ -223,7 +226,7 @@ function stateModelFactory() {
223
226
 
224
227
  /**
225
228
  * #property
226
- * filehandle object for InterProScan GFF file
229
+ * filehandle object for a GFF file of overlay annotations
227
230
  */
228
231
  gffFilehandle: types.maybe(FileLocation),
229
232
 
@@ -385,10 +388,12 @@ function stateModelFactory() {
385
388
 
386
389
  /**
387
390
  * #volatile
391
+ * overlay annotations drawn on the alignment, whatever their source.
392
+ * Every source -- InterProScan, GFF, a user upload -- converts to this
393
+ * flat list before it reaches the model, so nothing downstream of here
394
+ * knows which one it came from
388
395
  */
389
- interProAnnotations: undefined as
390
- | undefined
391
- | Record<string, InterProScanResults>,
396
+ annotations: [] as Annotation[],
392
397
  }))
393
398
  .actions(self => ({
394
399
  /**
@@ -480,7 +485,7 @@ function stateModelFactory() {
480
485
  },
481
486
  /**
482
487
  * #action
483
- * toggle the InterProScan protein-domain overlay on the alignment
488
+ * toggle the annotation overlay on the alignment
484
489
  */
485
490
  setShowDomains(arg: boolean) {
486
491
  self.showDomains = arg
@@ -523,14 +528,6 @@ function stateModelFactory() {
523
528
  self.colWidth = n
524
529
  },
525
530
 
526
- /**
527
- * #action
528
- * set scroll Y-offset (px)
529
- */
530
- setScrollY(n: number) {
531
- self.scrollY = n
532
- },
533
-
534
531
  /**
535
532
  * #action
536
533
  *
@@ -638,7 +635,7 @@ function stateModelFactory() {
638
635
  * #getter
639
636
  */
640
637
  get actuallyShowDomains() {
641
- return self.showDomains && !!self.interProAnnotations
638
+ return self.showDomains && self.annotations.length > 0
642
639
  },
643
640
  get viewInitialized() {
644
641
  return self.volatileWidth !== undefined
@@ -693,7 +690,7 @@ function stateModelFactory() {
693
690
  return !!this.tree.noTree
694
691
  },
695
692
  get noDomains() {
696
- return !self.interProAnnotations
693
+ return self.annotations.length === 0
697
694
  },
698
695
  menuItems() {
699
696
  return []
@@ -883,12 +880,6 @@ function stateModelFactory() {
883
880
  }
884
881
  return blanks
885
882
  },
886
- /**
887
- * #getter
888
- */
889
- get blanksSet() {
890
- return new Set(this.blanks)
891
- },
892
883
  /**
893
884
  * #getter
894
885
  * Returns a map of row name to array of insertions with display position and letters
@@ -981,14 +972,6 @@ function stateModelFactory() {
981
972
  return columnCountsFromRows(this.columns2d)
982
973
  },
983
974
 
984
- /**
985
- * #getter
986
- * per-column residue totals (gaps included)
987
- */
988
- get colStatsSums() {
989
- return this.colStats.totals
990
- },
991
-
992
975
  /**
993
976
  * #getter
994
977
  * Detects sequence type based on letters present in the alignment.
@@ -1096,14 +1079,13 @@ function stateModelFactory() {
1096
1079
  const max = maxLength(r)
1097
1080
  const k = max ? self.treeWidth / max : 0
1098
1081
  setBrLength(r, 0, k)
1099
- // for each collapsed clade, record the pixel x-positions of its
1100
- // nearest/farthest tips so the renderer can draw a triangle that spans
1101
- // the branch-length extent of the hidden subtree
1082
+ // for each collapsed clade, record the pixel x-position of its farthest
1083
+ // tip so the renderer can draw a triangle spanning the branch-length
1084
+ // extent of the hidden subtree
1102
1085
  forEachDescendant(r, node => {
1103
1086
  if (node._children) {
1104
- const { min, max: mx } = collapsedSubtreeLengthExtent(node)
1105
- node.collapsedTipXNear = (node.len ?? 0) + min * k
1106
- node.collapsedTipXFar = (node.len ?? 0) + mx * k
1087
+ node.collapsedTipXFar =
1088
+ (node.len ?? 0) + collapsedSubtreeMaxLength(node) * k
1107
1089
  }
1108
1090
  })
1109
1091
  return r as HierarchyNode<NodeWithIdsAndLength>
@@ -1265,17 +1247,24 @@ function stateModelFactory() {
1265
1247
  * set hovered tree node and its descendants
1266
1248
  */
1267
1249
  setHoveredTreeNode(nodeId?: string) {
1268
- if (nodeId) {
1269
- const node = find(self.hierarchy, n => n.data.id === nodeId)
1270
- self.hoveredTreeNode = node
1271
- ? {
1272
- nodeId,
1273
- descendantNames: leaves(node).map(leaf => leaf.data.name),
1274
- }
1275
- : undefined
1276
- } else {
1250
+ // the tree's mousemove handler calls this on every event, and both the
1251
+ // lookup and the write are expensive: `find` walks the whole hierarchy,
1252
+ // and a fresh object here invalidates hoveredRowIndices and redraws the
1253
+ // tree and MSA overlays. Re-hovering the same node is the common case
1254
+ if (nodeId === self.hoveredTreeNode?.nodeId) {
1255
+ return
1256
+ }
1257
+ if (!nodeId) {
1277
1258
  self.hoveredTreeNode = undefined
1259
+ return
1278
1260
  }
1261
+ const node = find(self.hierarchy, n => n.data.id === nodeId)
1262
+ self.hoveredTreeNode = node
1263
+ ? {
1264
+ nodeId,
1265
+ descendantNames: leaves(node).map(leaf => leaf.data.name),
1266
+ }
1267
+ : undefined
1279
1268
  },
1280
1269
 
1281
1270
  /**
@@ -1394,23 +1383,40 @@ function stateModelFactory() {
1394
1383
  * #action
1395
1384
  */
1396
1385
  doScrollY(deltaY: number) {
1397
- self.scrollY = clamp(self.scrollY + deltaY, self.maxScrollY, 0)
1386
+ this.setScrollY(self.scrollY + deltaY)
1398
1387
  },
1399
1388
 
1400
1389
  /**
1401
1390
  * #action
1402
- * Set domain annotations and reveal the overlay in a single step (or clear
1403
- * both when passed undefined). Shared by every domain source: InterProScan,
1404
- * GFF, user-provided uploads, and NCBI CDD.
1391
+ * set scroll Y-offset (px), clamped to keep the alignment in view
1392
+ */
1393
+ setScrollY(n: number) {
1394
+ self.scrollY = clamp(n, self.maxScrollY, 0)
1395
+ },
1396
+
1397
+ /**
1398
+ * #action
1399
+ * Set the overlay annotations and reveal the overlay in a single step (an
1400
+ * empty list clears both). Every source funnels through here after its
1401
+ * own adapter has flattened it: InterProScan, GFF, user uploads, NCBI CDD.
1402
+ */
1403
+ setAnnotations(annotations: Annotation[]) {
1404
+ self.annotations = annotations
1405
+ self.setShowDomains(annotations.length > 0)
1406
+ },
1407
+
1408
+ /**
1409
+ * #action
1410
+ * set the overlay from raw InterProScan results keyed by row name. Kept
1411
+ * for downstream plugins that hold the EBI wire format; new code should
1412
+ * adapt to Annotation[] and call setAnnotations.
1405
1413
  */
1406
1414
  setDomains(data?: Record<string, InterProScanResults>) {
1407
- self.interProAnnotations = data
1408
- self.setShowDomains(!!data)
1415
+ this.setAnnotations(data ? interProScanToAnnotations(data) : [])
1409
1416
  },
1410
1417
 
1411
1418
  applyGFFText(gffText: string) {
1412
- const gffRecords = parseGFF(gffText)
1413
- this.setDomains(gffToInterProResults(gffRecords))
1419
+ this.setAnnotations(gffToAnnotations(parseGFF(gffText)))
1414
1420
  },
1415
1421
 
1416
1422
  /**
@@ -1449,10 +1455,12 @@ function stateModelFactory() {
1449
1455
  * #getter
1450
1456
  */
1451
1457
  get labelWidthMap() {
1452
- const { rowHeight, leaves, treeMetadata, fontSize } = self
1453
- return rowHeight <= 5
1454
- ? new Map<string, number>()
1455
- : new Map(
1458
+ const { showTreeText, leaves, treeMetadata, fontSize } = self
1459
+ // gated on the same condition the renderer draws labels under, so the
1460
+ // gutter labelsWidth reserves and the labels actually drawn cannot
1461
+ // disagree -- and so turning labels off hands their space to the tree
1462
+ return showTreeText
1463
+ ? new Map(
1456
1464
  leaves.map(node => {
1457
1465
  const { name } = node.data
1458
1466
  // `||`, matching renderTreeLabels: an empty genome falls back
@@ -1462,6 +1470,7 @@ function stateModelFactory() {
1462
1470
  return [name, measureTextCanvas(displayName, fontSize)] as const
1463
1471
  }),
1464
1472
  )
1473
+ : new Map<string, number>()
1465
1474
  },
1466
1475
 
1467
1476
  get labelsWidth() {
@@ -1674,41 +1683,29 @@ function stateModelFactory() {
1674
1683
  get totalTrackAreaHeight() {
1675
1684
  return sum(self.turnedOnTracks.map(r => r.model.height))
1676
1685
  },
1677
- get tidyInterProAnnotationTypes() {
1678
- return new Map(
1679
- this.tidyInterProAnnotations.map(annot => [annot.accession, annot]),
1680
- )
1681
- },
1682
- get tidyInterProAnnotations() {
1683
- return self.interProAnnotations
1684
- ? Object.entries(self.interProAnnotations)
1685
- .flatMap(([id, val]) =>
1686
- val.matches.flatMap(({ signature, locations }) => {
1687
- const { entry } = signature
1688
- return entry
1689
- ? locations.map(({ start, end, strand }) => ({
1690
- id,
1691
- name: entry.name,
1692
- accession: entry.accession,
1693
- description: entry.description,
1694
- featureType: entry.featureType,
1695
- start,
1696
- end,
1697
- strand,
1698
- }))
1699
- : []
1700
- }),
1701
- )
1702
- .toSorted((a, b) => len(b) - len(a))
1703
- : []
1686
+ /**
1687
+ * one representative annotation per accession, which is what the legend,
1688
+ * the filter dialog and the palettes key off
1689
+ */
1690
+ get annotationTypes() {
1691
+ // first occurrence wins. The representative supplies only the name,
1692
+ // description and -- for ordinal segments -- the start that orders
1693
+ // them, and those agree across an accession's instances
1694
+ const types = new Map<string, Annotation>()
1695
+ for (const annot of self.annotations) {
1696
+ if (!types.has(annot.accession)) {
1697
+ types.set(annot.accession, annot)
1698
+ }
1699
+ }
1700
+ return types
1704
1701
  },
1705
- get tidyFilteredInterProAnnotations() {
1706
- return this.tidyInterProAnnotations.filter(r =>
1702
+ get filteredAnnotations() {
1703
+ return self.annotations.filter(r =>
1707
1704
  self.featureFilters.get(r.accession),
1708
1705
  )
1709
1706
  },
1710
- get tidyFilteredGatheredInterProAnnotations() {
1711
- return groupBy(this.tidyFilteredInterProAnnotations, r => r.id)
1707
+ get annotationsByRow() {
1708
+ return groupBy(this.filteredAnnotations, r => r.id)
1712
1709
  },
1713
1710
  }))
1714
1711
  .views(self => ({
@@ -1726,6 +1723,15 @@ function stateModelFactory() {
1726
1723
  get verticalScrollbarWidth() {
1727
1724
  return self.showVerticalScrollbar ? 20 : 0
1728
1725
  },
1726
+ /**
1727
+ * #getter
1728
+ * width of the alignment canvas itself: the msa area less the vertical
1729
+ * scrollbar sitting in it. Not usable from showHorizontalScrollbar, which
1730
+ * feeds msaAreaHeight -> showVerticalScrollbar and would close a cycle
1731
+ */
1732
+ get msaCanvasWidth() {
1733
+ return self.msaAreaWidth - this.verticalScrollbarWidth
1734
+ },
1729
1735
  /**
1730
1736
  * #getter
1731
1737
  * ordinal segment types (exons etc.), ordered by sequence position so
@@ -1733,7 +1739,7 @@ function stateModelFactory() {
1733
1739
  * labeled by number rather than each getting a distinct hue + legend row
1734
1740
  */
1735
1741
  get segmentDomainTypes() {
1736
- return [...self.tidyInterProAnnotationTypes.values()]
1742
+ return [...self.annotationTypes.values()]
1737
1743
  .filter(d => segmentFeatureTypes.has(d.featureType ?? ''))
1738
1744
  .toSorted((a, b) => a.start - b.start)
1739
1745
  },
@@ -1743,7 +1749,7 @@ function stateModelFactory() {
1743
1749
  * their own color and a legend entry
1744
1750
  */
1745
1751
  get categoricalDomainTypes() {
1746
- return [...self.tidyInterProAnnotationTypes.values()].filter(
1752
+ return [...self.annotationTypes.values()].filter(
1747
1753
  d => !segmentFeatureTypes.has(d.featureType ?? ''),
1748
1754
  )
1749
1755
  },
@@ -1795,34 +1801,47 @@ function stateModelFactory() {
1795
1801
 
1796
1802
  /**
1797
1803
  * #getter
1798
- * every filtered-on domain annotation resolved to the visible column span
1799
- * it is drawn across, keyed by row name and in draw order (largest first,
1800
- * as tidyInterProAnnotations orders them). Resolving these once here rather
1801
- * than inside each canvas block removes a per-feature, per-block sequence
1802
- * position conversion from every redraw, and gives the letter renderer the
1803
- * band colors it needs to keep residues readable on top of the boxes.
1804
+ * every filtered-on annotation resolved to the visible column span it is
1805
+ * drawn across, keyed by row name. Each row is ordered longest-first so a
1806
+ * short domain nested inside a long one draws on top of it rather than
1807
+ * under it. Resolving these once here rather than inside each canvas
1808
+ * block removes a per-feature, per-block sequence position conversion
1809
+ * from every redraw, and gives the letter renderer the band colors it
1810
+ * needs to keep residues readable on top of the boxes.
1804
1811
  */
1805
1812
  get domainBands() {
1806
1813
  const bands = new Map<string, DomainBand[]>()
1807
1814
  for (const [name, annotations] of Object.entries(
1808
- self.tidyFilteredGatheredInterProAnnotations,
1815
+ self.annotationsByRow,
1809
1816
  )) {
1817
+ const { blanks } = self
1810
1818
  const rowBands = annotations
1811
- .map((annotation, stackIndex) => {
1812
- // InterPro positions are 1-based and inclusive; endCol is the
1813
- // exclusive column *after* the last residue, taken from that
1814
- // residue rather than from the next one, so a band stops at its own
1815
- // last residue instead of stretching across a following gap run
1816
- const startCol = self.seqPosToVisibleCol(
1817
- name,
1818
- annotation.start - 1,
1819
+ .toSorted((a, b) => len(b) - len(a))
1820
+ .map(annotation => {
1821
+ // annotation positions are 1-based and inclusive. Both ends count
1822
+ // the visible columns in front of a global column, so endCol is
1823
+ // the exclusive column after the last residue's own column --
1824
+ // the band stops there rather than stretching across a following
1825
+ // gap run -- and a residue whose column is itself hidden
1826
+ // collapses onto the neighbouring boundary instead of dropping
1827
+ // the band. A band whose every column is hidden spans nothing
1828
+ // and is left out.
1829
+ const startCol = visibleColsBefore(
1830
+ blanks,
1831
+ self.seqPosToGlobalCol(name, annotation.start - 1),
1832
+ )
1833
+ const endCol = visibleColsBefore(
1834
+ blanks,
1835
+ self.seqPosToGlobalCol(name, annotation.end - 1) + 1,
1819
1836
  )
1820
- const lastCol = self.seqPosToVisibleCol(name, annotation.end - 1)
1821
- return startCol !== undefined && lastCol !== undefined
1822
- ? { annotation, startCol, endCol: lastCol + 1, stackIndex }
1837
+ return endCol > startCol
1838
+ ? { annotation, startCol, endCol }
1823
1839
  : undefined
1824
1840
  })
1825
1841
  .filter(notEmpty)
1842
+ // numbered after the drop, so a band that resolved to nothing does
1843
+ // not leave an empty sub-row behind it
1844
+ .map((band, stackIndex) => ({ ...band, stackIndex }))
1826
1845
  if (rowBands.length > 0) {
1827
1846
  bands.set(name, rowBands)
1828
1847
  }
@@ -1985,7 +2004,7 @@ function stateModelFactory() {
1985
2004
  self.setTreeFilehandle(undefined)
1986
2005
  self.setMSAFilehandle(undefined)
1987
2006
  self.setGFFFilehandle(undefined)
1988
- self.setDomains(undefined)
2007
+ self.setAnnotations([])
1989
2008
  },
1990
2009
  /**
1991
2010
  * #action
@@ -2035,8 +2054,11 @@ function stateModelFactory() {
2035
2054
  */
2036
2055
  fitHorizontally() {
2037
2056
  if (self.numColumns > 0) {
2057
+ // fitting to msaAreaWidth instead left the last ~20px of columns off
2058
+ // the right edge -- and short of the width that shows a minimap, so
2059
+ // nothing on screen said they were there
2038
2060
  self.colWidth = clamp(
2039
- self.msaAreaWidth / self.numColumns,
2061
+ self.msaCanvasWidth / self.numColumns,
2040
2062
  minColWidth,
2041
2063
  maxCellSize,
2042
2064
  )
@@ -2055,7 +2077,7 @@ function stateModelFactory() {
2055
2077
  addDisposer(
2056
2078
  self,
2057
2079
  autorun(() => {
2058
- for (const key of self.tidyInterProAnnotationTypes.keys()) {
2080
+ for (const key of self.annotationTypes.keys()) {
2059
2081
  this.initFilter(key)
2060
2082
  }
2061
2083
  }),
@@ -2195,7 +2217,7 @@ function stateModelFactory() {
2195
2217
  }),
2196
2218
  )
2197
2219
 
2198
- // gffFilehandle carries InterProScan domains
2220
+ // gffFilehandle carries overlay annotations
2199
2221
  loadOnFilehandleChange({
2200
2222
  getFilehandle: () => self.gffFilehandle,
2201
2223
  onLoad: text => {
@@ -0,0 +1,109 @@
1
+ // @vitest-environment jsdom
2
+ //
3
+ // The viewport export draws a minimap by default, and nothing covered it. The
4
+ // export lays the figure out on its own grid rather than the on-screen one, so
5
+ // the minimap has to be told how wide to draw -- reading a width off the model
6
+ // puts its trapezoid short of the alignment column beside it.
7
+ import { createJBrowseTheme } from '@jbrowse/core/ui/theme'
8
+ import { enableStaticRendering } from 'mobx-react'
9
+ import { beforeAll, expect, test } from 'vitest'
10
+
11
+ import MSAModelF from './model.ts'
12
+ import { renderToSvg } from './renderToSvg.tsx'
13
+
14
+ class Mat {
15
+ constructor(
16
+ public a = 1,
17
+ public b = 0,
18
+ public c = 0,
19
+ public d = 1,
20
+ public e = 0,
21
+ public f = 0,
22
+ ) {}
23
+ multiply(o: Mat) {
24
+ return new Mat(
25
+ this.a * o.a + this.c * o.b,
26
+ this.b * o.a + this.d * o.b,
27
+ this.a * o.c + this.c * o.d,
28
+ this.b * o.c + this.d * o.d,
29
+ this.a * o.e + this.c * o.f + this.e,
30
+ this.b * o.e + this.d * o.f + this.f,
31
+ )
32
+ }
33
+ translate(x: number, y = 0) {
34
+ return this.multiply(new Mat(1, 0, 0, 1, x, y))
35
+ }
36
+ scale(x: number, y = x) {
37
+ return this.multiply(new Mat(x, 0, 0, y, 0, 0))
38
+ }
39
+ }
40
+ class Pt {
41
+ constructor(
42
+ public x = 0,
43
+ public y = 0,
44
+ ) {}
45
+ matrixTransform(m: Mat) {
46
+ return new Pt(
47
+ m.a * this.x + m.c * this.y + m.e,
48
+ m.b * this.x + m.d * this.y + m.f,
49
+ )
50
+ }
51
+ }
52
+
53
+ beforeAll(() => {
54
+ enableStaticRendering(true)
55
+ const g = globalThis as Record<string, unknown>
56
+ g.DOMMatrix = Mat
57
+ g.DOMPoint = Pt
58
+ HTMLCanvasElement.prototype.getContext = function () {
59
+ let font = '10px sans-serif'
60
+ return {
61
+ get font() {
62
+ return font
63
+ },
64
+ set font(v: string) {
65
+ font = v
66
+ },
67
+ measureText: (t: string) => ({
68
+ width: t.length * (Number.parseFloat(font) || 10) * 0.6,
69
+ }),
70
+ } as unknown as CanvasRenderingContext2D
71
+ } as unknown as typeof HTMLCanvasElement.prototype.getContext
72
+ })
73
+
74
+ const width = 1000
75
+
76
+ function makeModel() {
77
+ const row = 'ACDEFGHIKL'.repeat(40)
78
+ const model = MSAModelF().create({
79
+ type: 'MsaView',
80
+ msaFormat: 'fasta',
81
+ height: 300,
82
+ data: { msa: `>seq1\n${row}\n>seq2\n${row}` },
83
+ })
84
+ model.setWidth(width)
85
+ return model
86
+ }
87
+
88
+ test('the exported minimap spans the exported alignment column', async () => {
89
+ const model = makeModel()
90
+ expect(model.showHorizontalScrollbar).toBe(true)
91
+
92
+ const svg = await renderToSvg(model, {
93
+ theme: createJBrowseTheme(),
94
+ exportType: 'viewport',
95
+ includeMinimap: true,
96
+ includeTracks: false,
97
+ })
98
+
99
+ // the minimap column runs from the tree area to the right edge of the figure
100
+ const minimapWidth = width - model.treeAreaWidth
101
+ const polygon = /<polygon[^>]*points="([^"]*)"/.exec(svg)?.[1]
102
+ expect(polygon).toBeDefined()
103
+ // bottom edge of the trapezoid reaches the full width it was given
104
+ expect(polygon!.endsWith(`${minimapWidth},${model.minimapHeight - 12}`)).toBe(
105
+ true,
106
+ )
107
+ // and the bar outline is drawn to the same width
108
+ expect(svg).toContain(`width="${minimapWidth}"`)
109
+ })
@@ -161,7 +161,7 @@ function MsaSvg({
161
161
  {includeMinimap ? (
162
162
  <>
163
163
  <g transform={`translate(${treeAreaWidth} 0)`}>
164
- <MinimapSVG model={model} />
164
+ <MinimapSVG model={model} width={width - treeAreaWidth} />
165
165
  </g>
166
166
  <g transform={`translate(0 ${minimapHeight})`}>{body}</g>
167
167
  </>
@@ -178,7 +178,7 @@ function MsaSvg({
178
178
  }
179
179
 
180
180
  // the domain color key drawn as a reserved column to the right of the
181
- // alignment, mirroring the on-screen DomainLegend overlay
181
+ // alignment, mirroring the on-screen AnnotationLegend overlay
182
182
  function LegendSVG({ model, width }: { model: MsaViewModel; width: number }) {
183
183
  const { visibleDomainTypes, fillPalette, strokePalette } = model
184
184
  const boxHeight = LEGEND_PAD * 2 + visibleDomainTypes.length * LEGEND_ROW_H