react-msaview 5.6.1 → 5.6.3

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 (94) hide show
  1. package/bundle/index.js +95 -95
  2. package/bundle/index.js.map +4 -4
  3. package/dist/clustalX.d.ts +4 -3
  4. package/dist/clustalX.js +25 -4
  5. package/dist/clustalX.js.map +1 -1
  6. package/dist/columnCounts.d.ts +37 -0
  7. package/dist/columnCounts.js +148 -0
  8. package/dist/columnCounts.js.map +1 -0
  9. package/dist/components/MSAView.js +1 -8
  10. package/dist/components/MSAView.js.map +1 -1
  11. package/dist/components/getVisibleLeaves.d.ts +6 -0
  12. package/dist/components/getVisibleLeaves.js.map +1 -0
  13. package/dist/components/msa/MSACanvasBlock.js +17 -54
  14. package/dist/components/msa/MSACanvasBlock.js.map +1 -1
  15. package/dist/components/msa/renderBoxFeatureCanvasBlock.d.ts +2 -1
  16. package/dist/components/msa/renderBoxFeatureCanvasBlock.js +21 -19
  17. package/dist/components/msa/renderBoxFeatureCanvasBlock.js.map +1 -1
  18. package/dist/components/msa/renderMSABlock.js +97 -96
  19. package/dist/components/msa/renderMSABlock.js.map +1 -1
  20. package/dist/components/msa/renderMSAMouseover.js +9 -32
  21. package/dist/components/msa/renderMSAMouseover.js.map +1 -1
  22. package/dist/components/msa/useMsaBlockMouse.d.ts +23 -0
  23. package/dist/components/msa/useMsaBlockMouse.js +45 -0
  24. package/dist/components/msa/useMsaBlockMouse.js.map +1 -0
  25. package/dist/components/tree/TreeCanvas.js +14 -19
  26. package/dist/components/tree/TreeCanvas.js.map +1 -1
  27. package/dist/components/tree/TreeCanvasBlock.js +40 -111
  28. package/dist/components/tree/TreeCanvasBlock.js.map +1 -1
  29. package/dist/components/tree/renderTreeCanvas.d.ts +2 -0
  30. package/dist/components/tree/renderTreeCanvas.js +15 -3
  31. package/dist/components/tree/renderTreeCanvas.js.map +1 -1
  32. package/dist/components/tree/useTreeHover.d.ts +24 -0
  33. package/dist/components/tree/useTreeHover.js +44 -0
  34. package/dist/components/tree/useTreeHover.js.map +1 -0
  35. package/dist/hierarchy.js +15 -9
  36. package/dist/hierarchy.js.map +1 -1
  37. package/dist/index.d.ts +2 -1
  38. package/dist/model/msaModel.js +4 -3
  39. package/dist/model/msaModel.js.map +1 -1
  40. package/dist/model/treeModel.js +9 -8
  41. package/dist/model/treeModel.js.map +1 -1
  42. package/dist/model.d.ts +52 -10
  43. package/dist/model.js +208 -93
  44. package/dist/model.js.map +1 -1
  45. package/dist/propertyConservation.d.ts +2 -1
  46. package/dist/propertyConservation.js +36 -25
  47. package/dist/propertyConservation.js.map +1 -1
  48. package/dist/seqPosToGlobalCol.d.ts +7 -0
  49. package/dist/seqPosToGlobalCol.js +17 -0
  50. package/dist/seqPosToGlobalCol.js.map +1 -1
  51. package/dist/stripDefault.d.ts +21 -0
  52. package/dist/stripDefault.js +26 -0
  53. package/dist/stripDefault.js.map +1 -0
  54. package/dist/types.d.ts +16 -0
  55. package/dist/version.d.ts +1 -1
  56. package/dist/version.js +1 -1
  57. package/package.json +3 -3
  58. package/src/clustalX.test.ts +9 -11
  59. package/src/clustalX.ts +27 -28
  60. package/src/columnCounts.test.ts +58 -0
  61. package/src/columnCounts.ts +170 -0
  62. package/src/components/MSAView.tsx +1 -9
  63. package/src/components/{msa/getVisibleLeaves.ts → getVisibleLeaves.ts} +1 -1
  64. package/src/components/msa/MSACanvasBlock.tsx +42 -110
  65. package/src/components/msa/renderBoxFeatureCanvasBlock.ts +25 -18
  66. package/src/components/msa/renderMSABlock.ts +113 -125
  67. package/src/components/msa/renderMSAMouseover.ts +11 -34
  68. package/src/components/msa/useMsaBlockMouse.ts +81 -0
  69. package/src/components/tree/TreeCanvas.tsx +16 -23
  70. package/src/components/tree/TreeCanvasBlock.tsx +62 -143
  71. package/src/components/tree/renderTreeCanvas.test.ts +100 -190
  72. package/src/components/tree/renderTreeCanvas.ts +14 -3
  73. package/src/components/tree/useTreeHover.ts +69 -0
  74. package/src/domainBands.test.ts +98 -0
  75. package/src/hierarchy.ts +15 -9
  76. package/src/index.ts +7 -0
  77. package/src/model/msaModel.ts +4 -6
  78. package/src/model/treeModel.ts +9 -14
  79. package/src/model.ts +241 -112
  80. package/src/propertyConservation.test.ts +2 -13
  81. package/src/propertyConservation.ts +40 -31
  82. package/src/rowSpace.test.ts +46 -0
  83. package/src/seqPosToGlobalCol.ts +18 -0
  84. package/src/stripDefault.test.ts +48 -0
  85. package/src/stripDefault.ts +35 -0
  86. package/src/types.ts +23 -0
  87. package/src/version.ts +1 -1
  88. package/dist/components/msa/getVisibleLeaves.d.ts +0 -6
  89. package/dist/components/msa/getVisibleLeaves.js.map +0 -1
  90. package/dist/useDevicePixelRatio.d.ts +0 -1
  91. package/dist/useDevicePixelRatio.js +0 -27
  92. package/dist/useDevicePixelRatio.js.map +0 -1
  93. package/src/useDevicePixelRatio.ts +0 -29
  94. /package/dist/components/{msa/getVisibleLeaves.js → getVisibleLeaves.js} +0 -0
@@ -0,0 +1,69 @@
1
+ import { useRef, useState } from 'react'
2
+ import type React from 'react'
3
+
4
+ import { ClickMapIndex } from './clickMap.ts'
5
+
6
+ import type { ClickEntry } from './clickMap.ts'
7
+ import type { MsaViewModel } from '../../model.ts'
8
+
9
+ export interface TreeHoverTarget extends ClickEntry {
10
+ clientX: number
11
+ clientY: number
12
+ }
13
+
14
+ /**
15
+ * Owns hit-testing for one tree block: the spatial index the render pass fills
16
+ * in, the entry currently under the pointer, and the model hover state that the
17
+ * MSA panel mirrors. The index is a render artifact rather than display state, so
18
+ * it lives in a ref; everything the component draws comes back as plain state.
19
+ */
20
+ export function useTreeHover({
21
+ model,
22
+ offsetY,
23
+ }: {
24
+ model: MsaViewModel
25
+ offsetY: number
26
+ }) {
27
+ const clickMapRef = useRef<ClickMapIndex>(null)
28
+ clickMapRef.current ??= new ClickMapIndex()
29
+ const clickMap = clickMapRef.current
30
+ const [hovered, setHovered] = useState<TreeHoverTarget>()
31
+
32
+ // leaf labels win over the branch/bubble targets they overlap, so a click on a
33
+ // name opens the node menu rather than the branch menu
34
+ function hitTest(event: React.MouseEvent) {
35
+ const x = event.nativeEvent.offsetX
36
+ const y = event.nativeEvent.offsetY + offsetY
37
+ const entries = clickMap.search({
38
+ minX: x,
39
+ maxX: x + 1,
40
+ minY: y,
41
+ maxY: y + 1,
42
+ })
43
+ return entries.find(entry => !entry.branch) ?? entries[0]
44
+ }
45
+
46
+ function onMouseMove(event: React.MouseEvent) {
47
+ const entry = hitTest(event)
48
+ setHovered(
49
+ entry
50
+ ? { ...entry, clientX: event.clientX, clientY: event.clientY }
51
+ : undefined,
52
+ )
53
+ // hovering an internal node highlights every tip below it; hovering a leaf
54
+ // label additionally drives the single-row highlight
55
+ model.setHoveredTreeNode(entry?.id)
56
+ model.setMousePos(
57
+ undefined,
58
+ entry && !entry.branch ? model.rowNamesSet.get(entry.name) : undefined,
59
+ )
60
+ }
61
+
62
+ function onMouseLeave() {
63
+ setHovered(undefined)
64
+ model.setHoveredTreeNode(undefined)
65
+ model.setMousePos(undefined, undefined)
66
+ }
67
+
68
+ return { clickMap, hovered, hitTest, onMouseMove, onMouseLeave }
69
+ }
@@ -0,0 +1,98 @@
1
+ // @vitest-environment jsdom
2
+ // The domain overlay resolves each annotation to the visible column span it is
3
+ // drawn across once, on the model, instead of converting sequence positions
4
+ // inside every canvas block. These tests pin that span (including across hidden
5
+ // gappy columns) and the hit-test built on top of it.
6
+ import { expect, test } from 'vitest'
7
+
8
+ import MSAModelF from './model.ts'
9
+
10
+ // row `b` has a 4-residue insertion relative to `a`, which becomes a run of
11
+ // mostly-gap columns that gap hiding can remove
12
+ const msa = `>a
13
+ MKLV----WYAC
14
+ >b
15
+ MKLVQQQQWYAC`
16
+
17
+ const gff = `##gff-version 3
18
+ a Pfam protein_match 1 4 . . . Name=PF00001;signature_desc=First
19
+ a Pfam protein_match 5 8 . . . Name=PF00002;signature_desc=Second`
20
+
21
+ function makeModel() {
22
+ const model = MSAModelF().create({
23
+ id: 'domain-bands-test',
24
+ type: 'MsaView',
25
+ msaFormat: 'fasta',
26
+ height: 400,
27
+ data: { msa },
28
+ })
29
+ model.setWidth(800)
30
+ model.applyGFFText(gff)
31
+ return model
32
+ }
33
+
34
+ test('bands span the columns their residues occupy', () => {
35
+ const model = makeModel()
36
+ const bands = model.domainBands.get('a')!
37
+ expect(
38
+ bands.map(b => [b.annotation.accession, b.startCol, b.endCol]),
39
+ ).toEqual([
40
+ ['PF00001', 0, 4],
41
+ ['PF00002', 8, 12],
42
+ ])
43
+ })
44
+
45
+ test('bands follow the columns when gappy columns are hidden', () => {
46
+ const model = makeModel()
47
+ model.setHideGaps(true)
48
+ model.setAllowedGappyness(50)
49
+ // the 4 insertion columns are gaps in half the rows, so they are hidden and
50
+ // everything to their right shifts left by 4
51
+ expect(model.blanks).toEqual([4, 5, 6, 7])
52
+ const bands = model.domainBands.get('a')!
53
+ expect(bands.map(b => [b.startCol, b.endCol])).toEqual([
54
+ [0, 4],
55
+ [4, 8],
56
+ ])
57
+ })
58
+
59
+ test('mouseOverDomains hit-tests against the drawn span', () => {
60
+ const model = makeModel()
61
+ const rowIndex = model.rowNames.indexOf('a')
62
+
63
+ model.setMousePos(0, rowIndex)
64
+ expect(model.mouseOverDomains.map(d => d.accession)).toEqual(['PF00001'])
65
+
66
+ // the columns between the two domains are the insertion gap: no domain there
67
+ model.setMousePos(5, rowIndex)
68
+ expect(model.mouseOverDomains).toEqual([])
69
+
70
+ model.setMousePos(8, rowIndex)
71
+ expect(model.mouseOverDomains.map(d => d.accession)).toEqual(['PF00002'])
72
+
73
+ // end column is exclusive
74
+ model.setMousePos(12, rowIndex)
75
+ expect(model.mouseOverDomains).toEqual([])
76
+
77
+ // the other row has no annotations of its own
78
+ model.setMousePos(0, model.rowNames.indexOf('b'))
79
+ expect(model.mouseOverDomains).toEqual([])
80
+ })
81
+
82
+ test('filtering a domain off removes its band', () => {
83
+ const model = makeModel()
84
+ model.setFilter('PF00001', false)
85
+ expect(model.domainBands.get('a')!.map(b => b.annotation.accession)).toEqual([
86
+ 'PF00002',
87
+ ])
88
+ })
89
+
90
+ test('highlighted columns collapse into contiguous runs', () => {
91
+ const model = makeModel()
92
+ model.setHighlightedColumns([5, 1, 2, 3, 9])
93
+ expect(model.highlightedColumnRuns).toEqual([
94
+ { start: 1, end: 3 },
95
+ { start: 5, end: 5 },
96
+ { start: 9, end: 9 },
97
+ ])
98
+ })
package/src/hierarchy.ts CHANGED
@@ -288,17 +288,23 @@ export function collapsedSubtreeLengthExtent<T extends { length?: number }>(
288
288
  // x. Memoizes onto node.depthToLeaf since the layout walks the tree repeatedly.
289
289
  // See https://github.com/emmanuelparadis/ape/blob/master/R/plot.phylo.R
290
290
  export function calcDepthToLeaf<T>(node: HierarchyNode<T>): number {
291
- const nodes = descendants(node)
292
- for (let i = nodes.length - 1; i >= 0; i--) {
293
- const n = nodes[i]!
294
- if (n.depthToLeaf === undefined) {
295
- let maxDepth = 0
296
- if (n.children) {
297
- for (const child of n.children) {
298
- maxDepth = Math.max(maxDepth, 1 + child.depthToLeaf!)
291
+ // the memo check has to guard the traversal, not just the arithmetic: the tree
292
+ // renderer asks for the depth of every node on every pass, and re-walking each
293
+ // subtree makes that quadratic (a 5000-tip caterpillar tree spent >1s per draw
294
+ // here). Once a node is memoized so are all of its descendants.
295
+ if (node.depthToLeaf === undefined) {
296
+ const nodes = descendants(node)
297
+ for (let i = nodes.length - 1; i >= 0; i--) {
298
+ const n = nodes[i]!
299
+ if (n.depthToLeaf === undefined) {
300
+ let maxDepth = 0
301
+ if (n.children) {
302
+ for (const child of n.children) {
303
+ maxDepth = Math.max(maxDepth, 1 + child.depthToLeaf!)
304
+ }
299
305
  }
306
+ n.depthToLeaf = maxDepth
300
307
  }
301
- n.depthToLeaf = maxDepth
302
308
  }
303
309
  }
304
310
  return node.depthToLeaf!
package/src/index.ts CHANGED
@@ -12,12 +12,19 @@ export { type MsaViewModel, default as MSAModelF } from './model.ts'
12
12
  export type { MSAFormat, MSAParserType } from 'msa-parsers'
13
13
  export type { HierarchyNode } from './hierarchy.ts'
14
14
  export type { InterProScanResults } from './launchInterProScan.ts'
15
+ // ColumnCounts, DomainBand and TidyDomainAnnotation surface in the inferred type
16
+ // of the composed state model, so a downstream plugin cannot emit declarations
17
+ // for its own stateModelFactory without being able to name them (TS2883).
18
+ // jbrowse-plugin-msaview already re-exports MSAFormat for exactly this reason.
19
+ export type { ColumnCounts } from './columnCounts.ts'
15
20
  export type {
16
21
  Accession,
17
22
  BasicTrack,
18
23
  BasicTrackModel,
24
+ DomainBand,
19
25
  Node,
20
26
  NodeWithIds,
21
27
  NodeWithIdsAndLength,
22
28
  TextTrackModel,
29
+ TidyDomainAnnotation,
23
30
  } from './types.ts'
@@ -5,6 +5,7 @@ import {
5
5
  defaultColorSchemeName,
6
6
  defaultShowColumnStats,
7
7
  } from '../constants.ts'
8
+ import { stripDefault } from '../stripDefault.ts'
8
9
 
9
10
  import type { MSAFormat } from 'msa-parsers'
10
11
 
@@ -20,23 +21,20 @@ export function MSAModelF() {
20
21
  * #property
21
22
  * draw MSA tiles with a background color
22
23
  */
23
- bgColor: types.stripDefault(types.boolean, defaultBgColor),
24
+ bgColor: stripDefault(types.boolean, defaultBgColor),
24
25
 
25
26
  /**
26
27
  * #property
27
28
  * default color scheme name
28
29
  */
29
- colorSchemeName: types.stripDefault(types.string, defaultColorSchemeName),
30
+ colorSchemeName: stripDefault(types.string, defaultColorSchemeName),
30
31
 
31
32
  /**
32
33
  * #property
33
34
  * show a per-column statistics tooltip (consensus, conservation, gaps,
34
35
  * residue distribution) while hovering the alignment
35
36
  */
36
- showColumnStats: types.stripDefault(
37
- types.boolean,
38
- defaultShowColumnStats,
39
- ),
37
+ showColumnStats: stripDefault(types.boolean, defaultShowColumnStats),
40
38
 
41
39
  /**
42
40
  * #property
@@ -9,6 +9,7 @@ import {
9
9
  defaultTreeAreaWidth,
10
10
  defaultTreeWidth,
11
11
  } from '../constants.ts'
12
+ import { stripDefault } from '../stripDefault.ts'
12
13
 
13
14
  /**
14
15
  * #stateModel Tree
@@ -19,28 +20,25 @@ export function TreeModelF() {
19
20
  /**
20
21
  * #property
21
22
  */
22
- drawLabels: types.stripDefault(types.boolean, defaultDrawLabels),
23
+ drawLabels: stripDefault(types.boolean, defaultDrawLabels),
23
24
  /**
24
25
  * #property
25
26
  * right-align the labels
26
27
  */
27
- labelsAlignRight: types.stripDefault(
28
- types.boolean,
29
- defaultLabelsAlignRight,
30
- ),
28
+ labelsAlignRight: stripDefault(types.boolean, defaultLabelsAlignRight),
31
29
 
32
30
  /**
33
31
  * #property
34
32
  * width of the area the tree is drawn in, px
35
33
  */
36
- treeAreaWidth: types.stripDefault(types.number, defaultTreeAreaWidth),
34
+ treeAreaWidth: stripDefault(types.number, defaultTreeAreaWidth),
37
35
 
38
36
  /**
39
37
  * #property
40
38
  * width of the tree within the treeArea, px. automatically synced to
41
39
  * fit within treeAreaWidth
42
40
  */
43
- treeWidth: types.stripDefault(types.number, defaultTreeWidth),
41
+ treeWidth: stripDefault(types.number, defaultTreeWidth),
44
42
 
45
43
  /**
46
44
  * #property
@@ -48,22 +46,19 @@ export function TreeModelF() {
48
46
  * lengths. if false, the layout is a "cladogram" that does not take into
49
47
  * account evolutionary distances
50
48
  */
51
- showBranchLen: types.stripDefault(types.boolean, defaultShowBranchLen),
49
+ showBranchLen: stripDefault(types.boolean, defaultShowBranchLen),
52
50
 
53
51
  /**
54
52
  * #property
55
53
  * draw tree, boolean
56
54
  */
57
- drawTree: types.stripDefault(types.boolean, defaultDrawTree),
55
+ drawTree: stripDefault(types.boolean, defaultDrawTree),
58
56
 
59
57
  /**
60
58
  * #property
61
59
  * draw clickable node bubbles on the tree
62
60
  */
63
- drawNodeBubbles: types.stripDefault(
64
- types.boolean,
65
- defaultDrawNodeBubbles,
66
- ),
61
+ drawNodeBubbles: stripDefault(types.boolean, defaultDrawNodeBubbles),
67
62
 
68
63
  /**
69
64
  * #property
@@ -71,7 +66,7 @@ export function TreeModelF() {
71
66
  * instead of using a fixed width. useful when there is no tree, so the
72
67
  * label gutter isn't padded out to the default 400px
73
68
  */
74
- autoTreeAreaWidth: types.stripDefault(types.boolean, false),
69
+ autoTreeAreaWidth: stripDefault(types.boolean, false),
75
70
  })
76
71
  .actions(self => ({
77
72
  /**