react-msaview 5.0.7 → 5.0.16

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 (174) hide show
  1. package/bundle/index.js +135 -35
  2. package/bundle/index.js.LICENSE.txt +1 -1
  3. package/bundle/index.js.map +1 -1
  4. package/dist/components/Checkbox2.js +3 -6
  5. package/dist/components/Checkbox2.js.map +1 -1
  6. package/dist/components/MSAViewer.d.ts +14 -0
  7. package/dist/components/MSAViewer.js +34 -0
  8. package/dist/components/MSAViewer.js.map +1 -0
  9. package/dist/components/SequenceTextArea.js +4 -4
  10. package/dist/components/SequenceTextArea.js.map +1 -1
  11. package/dist/components/Track.js +5 -24
  12. package/dist/components/Track.js.map +1 -1
  13. package/dist/components/dialogs/DomainDialog.js +2 -5
  14. package/dist/components/dialogs/DomainDialog.js.map +1 -1
  15. package/dist/components/dialogs/InterProScanDialog.js +7 -7
  16. package/dist/components/dialogs/InterProScanDialog.js.map +1 -1
  17. package/dist/components/dialogs/SettingsDialog.js +3 -19
  18. package/dist/components/dialogs/SettingsDialog.js.map +1 -1
  19. package/dist/components/header/ColorSchemeMenu.d.ts +6 -0
  20. package/dist/components/header/ColorSchemeMenu.js +19 -0
  21. package/dist/components/header/ColorSchemeMenu.js.map +1 -0
  22. package/dist/components/header/{ZoomStar.d.ts → FileMenu.d.ts} +2 -2
  23. package/dist/components/header/FileMenu.js +71 -0
  24. package/dist/components/header/FileMenu.js.map +1 -0
  25. package/dist/components/header/Header.js +8 -6
  26. package/dist/components/header/Header.js.map +1 -1
  27. package/dist/components/header/HeaderMenu.js +3 -145
  28. package/dist/components/header/HeaderMenu.js.map +1 -1
  29. package/dist/components/header/MSASettingsMenu.d.ts +6 -0
  30. package/dist/components/header/MSASettingsMenu.js +36 -0
  31. package/dist/components/header/MSASettingsMenu.js.map +1 -0
  32. package/dist/components/header/SettingsMenu.js +1 -21
  33. package/dist/components/header/SettingsMenu.js.map +1 -1
  34. package/dist/components/header/TreeSettingsMenu.d.ts +6 -0
  35. package/dist/components/header/TreeSettingsMenu.js +74 -0
  36. package/dist/components/header/TreeSettingsMenu.js.map +1 -0
  37. package/dist/components/header/ZoomMenu.js +0 -8
  38. package/dist/components/header/ZoomMenu.js.map +1 -1
  39. package/dist/components/header/getDomainsMenu.d.ts +31 -0
  40. package/dist/components/header/getDomainsMenu.js +75 -0
  41. package/dist/components/header/getDomainsMenu.js.map +1 -0
  42. package/dist/components/import/ImportFormExamples.js +22 -20
  43. package/dist/components/import/ImportFormExamples.js.map +1 -1
  44. package/dist/components/msa/MSACanvas.js +13 -84
  45. package/dist/components/msa/MSACanvas.js.map +1 -1
  46. package/dist/components/msa/MSACanvasBlock.js +1 -3
  47. package/dist/components/msa/MSACanvasBlock.js.map +1 -1
  48. package/dist/components/msa/renderMSABlock.js +2 -4
  49. package/dist/components/msa/renderMSABlock.js.map +1 -1
  50. package/dist/components/msa/renderMSAMouseover.js +1 -7
  51. package/dist/components/msa/renderMSAMouseover.js.map +1 -1
  52. package/dist/components/tree/TreeCanvas.js +14 -91
  53. package/dist/components/tree/TreeCanvas.js.map +1 -1
  54. package/dist/components/tree/TreeNodeMenu.js +5 -16
  55. package/dist/components/tree/TreeNodeMenu.js.map +1 -1
  56. package/dist/components/tree/renderTreeCanvas.js +55 -22
  57. package/dist/components/tree/renderTreeCanvas.js.map +1 -1
  58. package/dist/constants.d.ts +0 -2
  59. package/dist/constants.js +0 -2
  60. package/dist/constants.js.map +1 -1
  61. package/dist/fetchUtils.d.ts +0 -1
  62. package/dist/fetchUtils.js +0 -4
  63. package/dist/fetchUtils.js.map +1 -1
  64. package/dist/flatToTree.d.ts +0 -5
  65. package/dist/flatToTree.js +13 -30
  66. package/dist/flatToTree.js.map +1 -1
  67. package/dist/hierarchy.d.ts +29 -0
  68. package/dist/hierarchy.js +164 -0
  69. package/dist/hierarchy.js.map +1 -0
  70. package/dist/index.d.ts +2 -0
  71. package/dist/index.js +1 -0
  72. package/dist/index.js.map +1 -1
  73. package/dist/launchInterProScan.d.ts +0 -5
  74. package/dist/launchInterProScan.js +5 -3
  75. package/dist/launchInterProScan.js.map +1 -1
  76. package/dist/model/DataModel.d.ts +9 -0
  77. package/dist/model/DataModel.js +12 -1
  78. package/dist/model/DataModel.js.map +1 -1
  79. package/dist/model/msaModel.d.ts +3 -0
  80. package/dist/model/msaModel.js +0 -1
  81. package/dist/model/msaModel.js.map +1 -1
  82. package/dist/model/treeModel.d.ts +3 -6
  83. package/dist/model/treeModel.js +3 -15
  84. package/dist/model/treeModel.js.map +1 -1
  85. package/dist/model.d.ts +24 -77
  86. package/dist/model.js +118 -239
  87. package/dist/model.js.map +1 -1
  88. package/dist/neighborJoining.js +38 -629
  89. package/dist/neighborJoining.js.map +1 -1
  90. package/dist/parseAsn1.d.ts +0 -12
  91. package/dist/parseAsn1.js +125 -332
  92. package/dist/parseAsn1.js.map +1 -1
  93. package/dist/useWheelScroll.d.ts +8 -0
  94. package/dist/useWheelScroll.js +93 -0
  95. package/dist/useWheelScroll.js.map +1 -0
  96. package/dist/util.d.ts +1 -6
  97. package/dist/util.js +5 -34
  98. package/dist/util.js.map +1 -1
  99. package/dist/vendor/copyToClipboard.d.ts +1 -10
  100. package/dist/vendor/copyToClipboard.js +14 -109
  101. package/dist/vendor/copyToClipboard.js.map +1 -1
  102. package/dist/vendor/fileSaver.d.ts +1 -11
  103. package/dist/vendor/fileSaver.js +7 -76
  104. package/dist/vendor/fileSaver.js.map +1 -1
  105. package/dist/version.d.ts +1 -1
  106. package/dist/version.js +1 -1
  107. package/dist/version.js.map +1 -1
  108. package/package.json +10 -13
  109. package/src/collapseLogic.test.ts +115 -0
  110. package/src/components/Checkbox2.tsx +9 -18
  111. package/src/components/MSAViewer.tsx +67 -0
  112. package/src/components/SequenceTextArea.tsx +4 -4
  113. package/src/components/Track.tsx +10 -26
  114. package/src/components/dialogs/DomainDialog.tsx +4 -5
  115. package/src/components/dialogs/InterProScanDialog.tsx +7 -7
  116. package/src/components/dialogs/SettingsDialog.tsx +0 -37
  117. package/src/components/header/ColorSchemeMenu.tsx +35 -0
  118. package/src/components/header/FileMenu.tsx +84 -0
  119. package/src/components/header/Header.tsx +8 -6
  120. package/src/components/header/HeaderMenu.tsx +4 -155
  121. package/src/components/header/MSASettingsMenu.tsx +48 -0
  122. package/src/components/header/SettingsMenu.tsx +0 -23
  123. package/src/components/header/TreeSettingsMenu.tsx +96 -0
  124. package/src/components/header/ZoomMenu.tsx +0 -8
  125. package/src/components/header/getDomainsMenu.ts +83 -0
  126. package/src/components/import/ImportFormExamples.tsx +38 -35
  127. package/src/components/msa/MSACanvas.tsx +21 -91
  128. package/src/components/msa/MSACanvasBlock.tsx +1 -3
  129. package/src/components/msa/renderBoxFeatureCanvasBlock.ts +1 -1
  130. package/src/components/msa/renderMSABlock.ts +2 -5
  131. package/src/components/msa/renderMSAMouseover.ts +0 -6
  132. package/src/components/tree/TreeCanvas.tsx +35 -100
  133. package/src/components/tree/TreeNodeMenu.tsx +5 -14
  134. package/src/components/tree/renderTreeCanvas.test.ts +205 -0
  135. package/src/components/tree/renderTreeCanvas.ts +64 -27
  136. package/src/constants.ts +0 -2
  137. package/src/fetchUtils.ts +0 -5
  138. package/src/flatToTree.ts +20 -38
  139. package/src/hierarchy.test.ts +120 -0
  140. package/src/hierarchy.ts +221 -0
  141. package/src/index.ts +2 -0
  142. package/src/launchInterProScan.ts +4 -3
  143. package/src/model/DataModel.ts +12 -1
  144. package/src/model/msaModel.ts +0 -2
  145. package/src/model/treeModel.ts +2 -18
  146. package/src/model.ts +180 -278
  147. package/src/neighborJoining.ts +38 -628
  148. package/src/parseAsn1.test.ts +4 -1
  149. package/src/parseAsn1.ts +135 -405
  150. package/src/useWheelScroll.ts +109 -0
  151. package/src/util.ts +5 -50
  152. package/src/vendor/copyToClipboard.ts +14 -122
  153. package/src/vendor/fileSaver.ts +8 -105
  154. package/src/version.ts +1 -1
  155. package/dist/components/dialogs/AddTrackDialog.d.ts +0 -8
  156. package/dist/components/dialogs/AddTrackDialog.js +0 -30
  157. package/dist/components/dialogs/AddTrackDialog.js.map +0 -1
  158. package/dist/components/dialogs/TabPanel.d.ts +0 -6
  159. package/dist/components/dialogs/TabPanel.js +0 -6
  160. package/dist/components/dialogs/TabPanel.js.map +0 -1
  161. package/dist/components/header/ZoomStar.js +0 -40
  162. package/dist/components/header/ZoomStar.js.map +0 -1
  163. package/dist/layout.d.ts +0 -26
  164. package/dist/layout.js +0 -74
  165. package/dist/layout.js.map +0 -1
  166. package/dist/reparseTree.d.ts +0 -2
  167. package/dist/reparseTree.js +0 -15
  168. package/dist/reparseTree.js.map +0 -1
  169. package/src/components/dialogs/AddTrackDialog.tsx +0 -85
  170. package/src/components/dialogs/TabPanel.tsx +0 -19
  171. package/src/components/header/ZoomStar.tsx +0 -74
  172. package/src/createPaletteMap.test.ts +0 -57
  173. package/src/layout.ts +0 -118
  174. package/src/reparseTree.ts +0 -18
@@ -1,3 +1,6 @@
1
+ import { descendants, links } from '../../hierarchy.ts'
2
+
3
+ import type { HierarchyNode } from '../../hierarchy.ts'
1
4
  import type { MsaViewModel } from '../../model.ts'
2
5
  import type { Theme } from '@mui/material'
3
6
 
@@ -7,6 +10,53 @@ const extendBounds = 5
7
10
  const radius = 2.5
8
11
  const d = radius * 2
9
12
 
13
+ // Cladogram positioning algorithm based on ape package's plot.phylo
14
+ // Uses topological depth (steps to tips) instead of branch length for x-positioning
15
+ // This ensures all leaf nodes align at the same x-coordinate (rightmost position)
16
+ // See: https://github.com/emmanuelparadis/ape/blob/master/R/plot.phylo.R
17
+ function calcDepthToLeaf(node: HierarchyNode): number {
18
+ if (node.depthToLeaf !== undefined) {
19
+ return node.depthToLeaf
20
+ }
21
+ if (!node.children || node.children.length === 0) {
22
+ node.depthToLeaf = 0
23
+ } else {
24
+ let maxDepth = 0
25
+ for (const child of node.children) {
26
+ maxDepth = Math.max(maxDepth, 1 + calcDepthToLeaf(child))
27
+ }
28
+ node.depthToLeaf = maxDepth
29
+ }
30
+ return node.depthToLeaf
31
+ }
32
+
33
+ function findMaxBranchLen(node: HierarchyNode): number {
34
+ let maxLen = node.len || 0
35
+ if (node.children) {
36
+ for (const child of node.children) {
37
+ maxLen = Math.max(maxLen, findMaxBranchLen(child))
38
+ }
39
+ }
40
+ return maxLen
41
+ }
42
+
43
+ // Calculate node x-coordinate for both phylogram (with branch lengths) and cladogram (topology only) modes
44
+ // For cladograms: x = (maxDepthToLeaf - nodeDepthToLeaf) / maxDepthToLeaf * maxWidth
45
+ // This positions: leaves at maxWidth (rightmost), root at 0 (leftmost), internal nodes proportionally in between
46
+ // Matches ape's: xx <- max(xx) - xx (where xx is depth from each node to tips)
47
+ function getNodeX(
48
+ node: HierarchyNode,
49
+ showBranchLen: boolean,
50
+ maxBranchLen: number,
51
+ maxDepthToLeaf: number,
52
+ ): number | undefined {
53
+ if (showBranchLen) {
54
+ return node.len
55
+ }
56
+ const depthToLeaf = calcDepthToLeaf(node)
57
+ return ((maxDepthToLeaf - depthToLeaf) / maxDepthToLeaf) * maxBranchLen
58
+ }
59
+
10
60
  interface ClickEntry {
11
61
  name: string
12
62
  id: string
@@ -45,15 +95,14 @@ export function renderTree({
45
95
  const { hierarchy, showBranchLenEffective: showBranchLen, blockSize } = model
46
96
  const by = blockSizeYOverride || blockSize
47
97
  ctx.strokeStyle = theme.palette.text.primary
48
- for (const link of hierarchy.links()) {
98
+ const maxBranchLen = findMaxBranchLen(hierarchy)
99
+ const maxDepthToLeaf = calcDepthToLeaf(hierarchy)
100
+ for (const link of links(hierarchy)) {
49
101
  const { source, target } = link
50
- if (target.height === 0 && !showBranchLen) {
51
- continue
52
- }
53
102
  const sy = source.x!
54
103
  const ty = target.x!
55
- const tx = showBranchLen ? (target as { len?: number }).len : target.y
56
- const sx = showBranchLen ? (source as { len?: number }).len : source.y
104
+ const tx = getNodeX(target, showBranchLen, maxBranchLen, maxDepthToLeaf)
105
+ const sx = getNodeX(source, showBranchLen, maxBranchLen, maxDepthToLeaf)
57
106
  if (tx === undefined || sx === undefined) {
58
107
  continue
59
108
  }
@@ -94,8 +143,10 @@ export function renderNodeBubbles({
94
143
  marginLeft: ml,
95
144
  } = model
96
145
  const by = blockSizeYOverride || blockSize
97
- for (const node of hierarchy.descendants()) {
98
- const x = showBranchLen ? (node as { len?: number }).len : node.y
146
+ const maxBranchLen = findMaxBranchLen(hierarchy)
147
+ const maxDepthToLeaf = calcDepthToLeaf(hierarchy)
148
+ for (const node of descendants(hierarchy)) {
149
+ const x = getNodeX(node, showBranchLen, maxBranchLen, maxDepthToLeaf)
99
150
  if (x === undefined) {
100
151
  continue
101
152
  }
@@ -103,7 +154,7 @@ export function renderNodeBubbles({
103
154
  const y = node.x!
104
155
  const { id, name } = data
105
156
  if (
106
- node.height > 1 &&
157
+ node.height >= 1 &&
107
158
  y > offsetY - extendBounds &&
108
159
  y < offsetY + by + extendBounds
109
160
  ) {
@@ -146,18 +197,15 @@ export function renderTreeLabels({
146
197
  fontSize,
147
198
  showBranchLenEffective: showBranchLen,
148
199
  treeMetadata,
149
- hierarchy,
150
- collapsed,
151
- collapsedLeaves,
152
200
  blockSize,
153
201
  labelsAlignRight,
154
202
  drawTree,
155
203
  treeAreaWidth,
156
- treeWidth,
157
204
  treeAreaWidthMinusMargin,
158
205
  marginLeft,
159
206
  leaves,
160
207
  noTree,
208
+ hierarchy,
161
209
  } = model
162
210
  const by = blockSizeYOverride || blockSize
163
211
  const emHeight = ctx.measureText('M').width
@@ -167,13 +215,13 @@ export function renderTreeLabels({
167
215
  } else {
168
216
  ctx.textAlign = 'start'
169
217
  }
218
+ const maxBranchLen = findMaxBranchLen(hierarchy)
219
+ const maxDepthToLeaf = calcDepthToLeaf(hierarchy)
170
220
  for (const node of leaves) {
171
221
  const {
172
222
  data: { name, id },
173
223
  } = node
174
- const len = (node as { len?: number }).len
175
224
  const y = node.x!
176
- const x = node.y!
177
225
 
178
226
  const displayName = treeMetadata[name]?.genome || name
179
227
  if (y > offsetY - extendBounds && y < offsetY + by + extendBounds) {
@@ -181,18 +229,7 @@ export function renderTreeLabels({
181
229
  const yp = y + fontSize / 4
182
230
  let xp = 0
183
231
  if (!noTree) {
184
- xp = (showBranchLen ? len : x) || 0
185
- if (
186
- !showBranchLen &&
187
- !collapsed.includes(id) &&
188
- !collapsedLeaves.includes(id)
189
- ) {
190
- // this subtraction is a hack to compensate for the leafnode rendering
191
- // glitch (issue #71). the context is that an extra leaf node is added
192
- // so that 'collapsing/hiding leaf nodes is possible' but this causes
193
- // weird workarounds
194
- xp -= treeWidth / hierarchy.height
195
- }
232
+ xp = getNodeX(node, showBranchLen, maxBranchLen, maxDepthToLeaf) || 0
196
233
  }
197
234
 
198
235
  const { width } = ctx.measureText(displayName)
package/src/constants.ts CHANGED
@@ -8,7 +8,6 @@ export const defaultCurrentAlignment = 0
8
8
  export const defaultShowDomains = false
9
9
  export const defaultHideGaps = true
10
10
  export const defaultAllowedGappyness = 100
11
- export const defaultContrastLettering = true
12
11
  export const defaultSubFeatureRows = false
13
12
  export const defaultDrawMsaLetters = true
14
13
 
@@ -21,7 +20,6 @@ export const defaultDrawLabels = true
21
20
  export const defaultLabelsAlignRight = false
22
21
  export const defaultTreeAreaWidth = 400
23
22
  export const defaultTreeWidth = 300
24
- export const defaultTreeWidthMatchesArea = true
25
23
  export const defaultShowBranchLen = true
26
24
  export const defaultDrawTree = true
27
25
  export const defaultDrawNodeBubbles = true
package/src/fetchUtils.ts CHANGED
@@ -20,11 +20,6 @@ export async function jsonfetch<T>(url: string, args?: RequestInit) {
20
20
  return response.json() as T
21
21
  }
22
22
 
23
- export async function arraybufferfetch(url: string) {
24
- const res = await myfetch(url)
25
- return res.arrayBuffer()
26
- }
27
-
28
23
  export function timeout(time: number) {
29
24
  return new Promise(res => setTimeout(res, time))
30
25
  }
package/src/flatToTree.ts CHANGED
@@ -1,10 +1,8 @@
1
- // Define the input item interface
2
1
  interface FlatItem {
3
2
  id: number
4
3
  parent?: number
5
4
  }
6
5
 
7
- // Define the tree node interface
8
6
  interface TreeNode {
9
7
  id: string
10
8
  name: string
@@ -12,46 +10,30 @@ interface TreeNode {
12
10
  children: TreeNode[]
13
11
  }
14
12
 
15
- /**
16
- * Parses a flat list of items into a tree structure
17
- * @param items - Array of flat items with id and optional parent
18
- * @returns Array of root tree nodes
19
- */
20
13
  export function flatToTree(items: FlatItem[]): TreeNode {
21
- // Create a map to store all nodes by their id for quick lookup
22
- const nodeMap = new Map<number, TreeNode>()
14
+ const nodeMap = new Map(
15
+ items.map(item => [
16
+ item.id,
17
+ {
18
+ id: `${item.id}`,
19
+ name: `${item.id}`,
20
+ parent: item.parent !== undefined ? `${item.parent}` : undefined,
21
+ children: [] as TreeNode[],
22
+ },
23
+ ]),
24
+ )
23
25
 
24
- // First pass: Create all tree nodes
25
- items.forEach(item => {
26
- nodeMap.set(item.id, {
27
- ...item,
28
- id: `${item.id}`,
29
- name: `${item.id}`,
30
- parent: item.parent !== undefined ? `${item.parent}` : undefined,
31
- children: [],
32
- })
33
- })
34
-
35
- // Second pass: Build parent-child relationships
36
- const roots: TreeNode[] = []
37
-
38
- items.forEach(item => {
26
+ let root: TreeNode | undefined
27
+ for (const item of items) {
39
28
  const node = nodeMap.get(item.id)!
40
-
41
- if (item.parent !== undefined) {
42
- // This item has a parent, add it to parent's children
43
- const parentNode = nodeMap.get(item.parent)
44
- if (parentNode) {
45
- parentNode.children.push(node)
46
- } else {
47
- // Parent doesn't exist, treat as root
48
- roots.push(node)
49
- }
29
+ const parent =
30
+ item.parent !== undefined ? nodeMap.get(item.parent) : undefined
31
+ if (parent) {
32
+ parent.children.push(node)
50
33
  } else {
51
- // This item has no parent, it's a root node
52
- roots.push(node)
34
+ root ??= node
53
35
  }
54
- })
36
+ }
55
37
 
56
- return roots[0]!
38
+ return root!
57
39
  }
@@ -0,0 +1,120 @@
1
+ import { describe, expect, test } from 'vitest'
2
+
3
+ import { collapse, find, hierarchy, leaves, sort, sum } from './hierarchy.ts'
4
+
5
+ import type { NodeWithIds } from './types.ts'
6
+
7
+ function makeTree(): NodeWithIds {
8
+ return {
9
+ id: 'root',
10
+ name: 'root',
11
+ children: [
12
+ {
13
+ id: 'A',
14
+ name: 'A',
15
+ children: [
16
+ { id: 'A1', name: 'A1', children: [] },
17
+ { id: 'A2', name: 'A2', children: [] },
18
+ ],
19
+ },
20
+ {
21
+ id: 'B',
22
+ name: 'B',
23
+ children: [
24
+ { id: 'B1', name: 'B1', children: [] },
25
+ { id: 'B2', name: 'B2', children: [] },
26
+ ],
27
+ },
28
+ ],
29
+ }
30
+ }
31
+
32
+ describe('hierarchy', () => {
33
+ test('builds hierarchy from tree data', () => {
34
+ const h = hierarchy(makeTree(), d => d.children)
35
+ expect(h.data.id).toBe('root')
36
+ expect(h.children?.length).toBe(2)
37
+ expect(h.height).toBe(2)
38
+ })
39
+
40
+ test('sets parent references', () => {
41
+ const h = hierarchy(makeTree(), d => d.children)
42
+ expect(h.parent).toBeNull()
43
+ expect(h.children![0]!.parent).toBe(h)
44
+ expect(h.children![0]!.children![0]!.parent).toBe(h.children![0])
45
+ })
46
+ })
47
+
48
+ describe('leaves', () => {
49
+ test('returns leaf nodes', () => {
50
+ const h = hierarchy(makeTree(), d => d.children)
51
+ const l = leaves(h)
52
+ expect(l.map(n => n.data.name)).toEqual(['A1', 'A2', 'B1', 'B2'])
53
+ })
54
+ })
55
+
56
+ describe('find', () => {
57
+ test('finds node by id', () => {
58
+ const h = hierarchy(makeTree(), d => d.children)
59
+ const node = find(h, n => n.data.id === 'B1')
60
+ expect(node?.data.name).toBe('B1')
61
+ })
62
+
63
+ test('returns undefined for missing id', () => {
64
+ const h = hierarchy(makeTree(), d => d.children)
65
+ expect(find(h, n => n.data.id === 'Z')).toBeUndefined()
66
+ })
67
+ })
68
+
69
+ describe('collapse', () => {
70
+ test('hides children of a branch node', () => {
71
+ const h = hierarchy(makeTree(), d => d.children)
72
+ const nodeA = find(h, n => n.data.id === 'A')!
73
+ collapse(nodeA)
74
+ expect(nodeA.children).toBeNull()
75
+ expect(nodeA._children?.length).toBe(2)
76
+ expect(leaves(h).map(n => n.data.name)).toEqual(['A', 'B1', 'B2'])
77
+ })
78
+
79
+ test('does nothing on a leaf node', () => {
80
+ const h = hierarchy(makeTree(), d => d.children)
81
+ const leaf = find(h, n => n.data.id === 'A1')!
82
+ collapse(leaf)
83
+ expect(leaves(h).map(n => n.data.name)).toEqual(['A1', 'A2', 'B1', 'B2'])
84
+ })
85
+ })
86
+
87
+ describe('leaf removal via parent.children filter', () => {
88
+ test('removing a leaf from its parent hides it from leaves()', () => {
89
+ const h = hierarchy(makeTree(), d => d.children)
90
+ const leaf = find(h, n => n.data.id === 'A1')!
91
+ leaf.parent!.children = leaf.parent!.children!.filter(
92
+ c => c.data.id !== 'A1',
93
+ )
94
+ expect(leaves(h).map(n => n.data.name)).toEqual(['A2', 'B1', 'B2'])
95
+ })
96
+
97
+ test('removing all leaves from a branch makes the branch a leaf', () => {
98
+ const h = hierarchy(makeTree(), d => d.children)
99
+ const nodeA = find(h, n => n.data.id === 'A')!
100
+ nodeA.children = []
101
+ expect(leaves(h).map(n => n.data.name)).toEqual(['B1', 'B2'])
102
+ })
103
+ })
104
+
105
+ describe('sort', () => {
106
+ test('sorts children', () => {
107
+ const h = hierarchy(makeTree(), d => d.children)
108
+ sort(h, (a, b) => b.data.name.localeCompare(a.data.name))
109
+ expect(h.children!.map(c => c.data.name)).toEqual(['B', 'A'])
110
+ })
111
+ })
112
+
113
+ describe('sum', () => {
114
+ test('computes leaf counts', () => {
115
+ const h = hierarchy(makeTree(), d => d.children)
116
+ sum(h, d => (d.children.length > 0 ? 0 : 1))
117
+ expect(h.value).toBe(4)
118
+ expect(find(h, n => n.data.id === 'A')?.value).toBe(2)
119
+ })
120
+ })
@@ -0,0 +1,221 @@
1
+ import type { NodeWithIds } from './types.ts'
2
+
3
+ export interface HierarchyNode<T = NodeWithIds> {
4
+ data: T
5
+ children: HierarchyNode<T>[] | null
6
+ parent: HierarchyNode<T> | null
7
+ depth: number
8
+ height: number
9
+ value?: number
10
+ x?: number
11
+ y?: number
12
+ len?: number
13
+ depthToLeaf?: number
14
+ _children?: HierarchyNode<T>[] | null
15
+ }
16
+
17
+ export interface HierarchyLink<T = NodeWithIds> {
18
+ source: HierarchyNode<T>
19
+ target: HierarchyNode<T>
20
+ }
21
+
22
+ function computeHeight<T>(node: HierarchyNode<T>): number {
23
+ let h = 0
24
+ if (node.children) {
25
+ for (const child of node.children) {
26
+ const ch = computeHeight(child) + 1
27
+ if (ch > h) {
28
+ h = ch
29
+ }
30
+ }
31
+ }
32
+ node.height = h
33
+ return h
34
+ }
35
+
36
+ function wrap<T>(
37
+ data: T,
38
+ childrenAccessor: (d: T) => T[] | undefined,
39
+ parent: HierarchyNode<T> | null,
40
+ depth: number,
41
+ ): HierarchyNode<T> {
42
+ const kids = childrenAccessor(data)
43
+ const node: HierarchyNode<T> = {
44
+ data,
45
+ children: null,
46
+ parent,
47
+ depth,
48
+ height: 0,
49
+ }
50
+ if (kids?.length) {
51
+ node.children = kids.map(d => wrap(d, childrenAccessor, node, depth + 1))
52
+ }
53
+ return node
54
+ }
55
+
56
+ export function hierarchy<T>(
57
+ data: T,
58
+ childrenAccessor: (d: T) => T[] | undefined,
59
+ ): HierarchyNode<T> {
60
+ const root = wrap(data, childrenAccessor, null, 0)
61
+ computeHeight(root)
62
+ return root
63
+ }
64
+
65
+ export function sum<T>(
66
+ node: HierarchyNode<T>,
67
+ valueFn: (d: T) => number,
68
+ ): HierarchyNode<T> {
69
+ function visit(n: HierarchyNode<T>): number {
70
+ let s = valueFn(n.data)
71
+ if (n.children) {
72
+ for (const child of n.children) {
73
+ s += visit(child)
74
+ }
75
+ }
76
+ n.value = s
77
+ return s
78
+ }
79
+ visit(node)
80
+ return node
81
+ }
82
+
83
+ export function sort<T>(
84
+ node: HierarchyNode<T>,
85
+ compareFn: (a: HierarchyNode<T>, b: HierarchyNode<T>) => number,
86
+ ): HierarchyNode<T> {
87
+ function visit(n: HierarchyNode<T>) {
88
+ if (n.children) {
89
+ n.children.sort(compareFn)
90
+ for (const child of n.children) {
91
+ visit(child)
92
+ }
93
+ }
94
+ }
95
+ visit(node)
96
+ return node
97
+ }
98
+
99
+ export function find<T>(
100
+ node: HierarchyNode<T>,
101
+ predicate: (n: HierarchyNode<T>) => boolean,
102
+ ): HierarchyNode<T> | undefined {
103
+ if (predicate(node)) {
104
+ return node
105
+ }
106
+ if (node.children) {
107
+ for (const child of node.children) {
108
+ const result = find(child, predicate)
109
+ if (result) {
110
+ return result
111
+ }
112
+ }
113
+ }
114
+ return undefined
115
+ }
116
+
117
+ export function leaves<T>(node: HierarchyNode<T>): HierarchyNode<T>[] {
118
+ const result: HierarchyNode<T>[] = []
119
+ function visit(n: HierarchyNode<T>) {
120
+ if (n.children) {
121
+ for (const child of n.children) {
122
+ visit(child)
123
+ }
124
+ } else {
125
+ result.push(n)
126
+ }
127
+ }
128
+ visit(node)
129
+ return result
130
+ }
131
+
132
+ export function descendants<T>(node: HierarchyNode<T>): HierarchyNode<T>[] {
133
+ const result: HierarchyNode<T>[] = []
134
+ function visit(n: HierarchyNode<T>) {
135
+ result.push(n)
136
+ if (n.children) {
137
+ for (const child of n.children) {
138
+ visit(child)
139
+ }
140
+ }
141
+ }
142
+ visit(node)
143
+ return result
144
+ }
145
+
146
+ export function links<T>(node: HierarchyNode<T>): HierarchyLink<T>[] {
147
+ const result: HierarchyLink<T>[] = []
148
+ function visit(n: HierarchyNode<T>) {
149
+ if (n.children) {
150
+ for (const child of n.children) {
151
+ result.push({ source: n, target: child })
152
+ visit(child)
153
+ }
154
+ }
155
+ }
156
+ visit(node)
157
+ return result
158
+ }
159
+
160
+ export function clusterLayout<T>(
161
+ root: HierarchyNode<T>,
162
+ sizeX: number,
163
+ sizeY: number,
164
+ ) {
165
+ const leafNodes = leaves(root)
166
+ const n = leafNodes.length
167
+ const step = sizeX / n
168
+
169
+ for (let i = 0; i < n; i++) {
170
+ leafNodes[i]!.x = (i + 0.5) * step
171
+ }
172
+
173
+ function assignX(node: HierarchyNode<T>) {
174
+ if (!node.children) {
175
+ return
176
+ }
177
+ for (const child of node.children) {
178
+ assignX(child)
179
+ }
180
+ let sum = 0
181
+ for (const child of node.children) {
182
+ sum += child.x!
183
+ }
184
+ node.x = sum / node.children.length
185
+ }
186
+ assignX(root)
187
+
188
+ const rootHeight = root.height
189
+ function assignY(node: HierarchyNode<T>, depth: number) {
190
+ node.y = rootHeight === 0 ? sizeY : (depth / rootHeight) * sizeY
191
+ if (node.children) {
192
+ for (const child of node.children) {
193
+ assignY(child, depth + 1)
194
+ }
195
+ }
196
+ }
197
+ assignY(root, 0)
198
+ }
199
+
200
+ export function collapse<T>(node: HierarchyNode<T>) {
201
+ if (node.children) {
202
+ node._children = node.children
203
+ node.children = null
204
+ }
205
+ }
206
+
207
+ export function maxLength(d: HierarchyNode): number {
208
+ return (
209
+ (d.data.length || 0) +
210
+ (d.children ? d.children.reduce((m, c) => Math.max(m, maxLength(c)), 0) : 0)
211
+ )
212
+ }
213
+
214
+ export function setBrLength(d: HierarchyNode, y0: number, k: number) {
215
+ d.len = (y0 += Math.max(d.data.length || 0, 0)) * k
216
+ if (d.children) {
217
+ for (const child of d.children) {
218
+ setBrLength(child, y0, k)
219
+ }
220
+ }
221
+ }
package/src/index.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  export { default as MSAView } from './components/Loading.tsx'
2
+ export { default as MSAViewer } from './components/MSAViewer.tsx'
2
3
  export { type MsaViewModel, default as MSAModelF } from './model.ts'
3
4
  export type { MSAParserType } from 'msa-parsers'
5
+ export type { HierarchyNode } from './hierarchy.ts'
4
6
  export type { InterProScanResults } from './launchInterProScan.ts'
5
7
  export type {
6
8
  Accession,
@@ -52,7 +52,7 @@ async function runInterProScan({
52
52
  await loadInterProScanResultsWithStatus({ jobId, model })
53
53
  }
54
54
 
55
- export function loadInterProScanResults(jobId: string) {
55
+ function loadInterProScanResults(jobId: string) {
56
56
  return jsonfetch<InterProScanResponse>(
57
57
  `${base}/iprscan5/result/${jobId}/json`,
58
58
  )
@@ -111,14 +111,15 @@ export async function launchInterProScan({
111
111
  programs,
112
112
  model,
113
113
  })
114
+ } else {
115
+ throw new Error('unknown algorithm')
114
116
  }
115
- throw new Error('unknown algorithm')
116
117
  } finally {
117
118
  onProgress()
118
119
  }
119
120
  }
120
121
 
121
- export async function loadInterProScanResultsWithStatus({
122
+ async function loadInterProScanResultsWithStatus({
122
123
  jobId,
123
124
  model,
124
125
  }: {
@@ -22,6 +22,10 @@ export function DataModelF() {
22
22
  * #property
23
23
  */
24
24
  treeMetadata: types.maybe(types.string),
25
+ /**
26
+ * #property
27
+ */
28
+ gff: types.maybe(types.string),
25
29
  })
26
30
  .actions(self => ({
27
31
  /**
@@ -42,15 +46,22 @@ export function DataModelF() {
42
46
  setTreeMetadata(treeMetadata?: string) {
43
47
  self.treeMetadata = treeMetadata
44
48
  },
49
+ /**
50
+ * #action
51
+ */
52
+ setGFF(gff?: string) {
53
+ self.gff = gff
54
+ },
45
55
  }))
46
56
  .postProcessSnapshot(snap => {
47
- const { tree, msa, treeMetadata } = snap
57
+ const { tree, msa, treeMetadata, gff } = snap
48
58
  const max = 50_000
49
59
  return {
50
60
  tree: tree && tree.length > max ? undefined : tree,
51
61
  msa: msa && msa.length > max ? undefined : msa,
52
62
  treeMetadata:
53
63
  treeMetadata && treeMetadata.length > max ? undefined : treeMetadata,
64
+ gff: gff && gff.length > max ? undefined : gff,
54
65
  }
55
66
  })
56
67
  }
@@ -5,8 +5,6 @@ import { defaultBgColor, defaultColorSchemeName } from '../constants.ts'
5
5
  /**
6
6
  * #stateModel MSAModel
7
7
  */
8
- function x() {} // eslint-disable-line @typescript-eslint/no-unused-vars
9
-
10
8
  export function MSAModelF() {
11
9
  return types
12
10
  .model({