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.
- package/bundle/index.js +135 -35
- package/bundle/index.js.LICENSE.txt +1 -1
- package/bundle/index.js.map +1 -1
- package/dist/components/Checkbox2.js +3 -6
- package/dist/components/Checkbox2.js.map +1 -1
- package/dist/components/MSAViewer.d.ts +14 -0
- package/dist/components/MSAViewer.js +34 -0
- package/dist/components/MSAViewer.js.map +1 -0
- package/dist/components/SequenceTextArea.js +4 -4
- package/dist/components/SequenceTextArea.js.map +1 -1
- package/dist/components/Track.js +5 -24
- package/dist/components/Track.js.map +1 -1
- package/dist/components/dialogs/DomainDialog.js +2 -5
- package/dist/components/dialogs/DomainDialog.js.map +1 -1
- package/dist/components/dialogs/InterProScanDialog.js +7 -7
- package/dist/components/dialogs/InterProScanDialog.js.map +1 -1
- package/dist/components/dialogs/SettingsDialog.js +3 -19
- package/dist/components/dialogs/SettingsDialog.js.map +1 -1
- package/dist/components/header/ColorSchemeMenu.d.ts +6 -0
- package/dist/components/header/ColorSchemeMenu.js +19 -0
- package/dist/components/header/ColorSchemeMenu.js.map +1 -0
- package/dist/components/header/{ZoomStar.d.ts → FileMenu.d.ts} +2 -2
- package/dist/components/header/FileMenu.js +71 -0
- package/dist/components/header/FileMenu.js.map +1 -0
- package/dist/components/header/Header.js +8 -6
- package/dist/components/header/Header.js.map +1 -1
- package/dist/components/header/HeaderMenu.js +3 -145
- package/dist/components/header/HeaderMenu.js.map +1 -1
- package/dist/components/header/MSASettingsMenu.d.ts +6 -0
- package/dist/components/header/MSASettingsMenu.js +36 -0
- package/dist/components/header/MSASettingsMenu.js.map +1 -0
- package/dist/components/header/SettingsMenu.js +1 -21
- package/dist/components/header/SettingsMenu.js.map +1 -1
- package/dist/components/header/TreeSettingsMenu.d.ts +6 -0
- package/dist/components/header/TreeSettingsMenu.js +74 -0
- package/dist/components/header/TreeSettingsMenu.js.map +1 -0
- package/dist/components/header/ZoomMenu.js +0 -8
- package/dist/components/header/ZoomMenu.js.map +1 -1
- package/dist/components/header/getDomainsMenu.d.ts +31 -0
- package/dist/components/header/getDomainsMenu.js +75 -0
- package/dist/components/header/getDomainsMenu.js.map +1 -0
- package/dist/components/import/ImportFormExamples.js +22 -20
- package/dist/components/import/ImportFormExamples.js.map +1 -1
- package/dist/components/msa/MSACanvas.js +13 -84
- package/dist/components/msa/MSACanvas.js.map +1 -1
- package/dist/components/msa/MSACanvasBlock.js +1 -3
- package/dist/components/msa/MSACanvasBlock.js.map +1 -1
- package/dist/components/msa/renderMSABlock.js +2 -4
- package/dist/components/msa/renderMSABlock.js.map +1 -1
- package/dist/components/msa/renderMSAMouseover.js +1 -7
- package/dist/components/msa/renderMSAMouseover.js.map +1 -1
- package/dist/components/tree/TreeCanvas.js +14 -91
- package/dist/components/tree/TreeCanvas.js.map +1 -1
- package/dist/components/tree/TreeNodeMenu.js +5 -16
- package/dist/components/tree/TreeNodeMenu.js.map +1 -1
- package/dist/components/tree/renderTreeCanvas.js +55 -22
- package/dist/components/tree/renderTreeCanvas.js.map +1 -1
- package/dist/constants.d.ts +0 -2
- package/dist/constants.js +0 -2
- package/dist/constants.js.map +1 -1
- package/dist/fetchUtils.d.ts +0 -1
- package/dist/fetchUtils.js +0 -4
- package/dist/fetchUtils.js.map +1 -1
- package/dist/flatToTree.d.ts +0 -5
- package/dist/flatToTree.js +13 -30
- package/dist/flatToTree.js.map +1 -1
- package/dist/hierarchy.d.ts +29 -0
- package/dist/hierarchy.js +164 -0
- package/dist/hierarchy.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/launchInterProScan.d.ts +0 -5
- package/dist/launchInterProScan.js +5 -3
- package/dist/launchInterProScan.js.map +1 -1
- package/dist/model/DataModel.d.ts +9 -0
- package/dist/model/DataModel.js +12 -1
- package/dist/model/DataModel.js.map +1 -1
- package/dist/model/msaModel.d.ts +3 -0
- package/dist/model/msaModel.js +0 -1
- package/dist/model/msaModel.js.map +1 -1
- package/dist/model/treeModel.d.ts +3 -6
- package/dist/model/treeModel.js +3 -15
- package/dist/model/treeModel.js.map +1 -1
- package/dist/model.d.ts +24 -77
- package/dist/model.js +118 -239
- package/dist/model.js.map +1 -1
- package/dist/neighborJoining.js +38 -629
- package/dist/neighborJoining.js.map +1 -1
- package/dist/parseAsn1.d.ts +0 -12
- package/dist/parseAsn1.js +125 -332
- package/dist/parseAsn1.js.map +1 -1
- package/dist/useWheelScroll.d.ts +8 -0
- package/dist/useWheelScroll.js +93 -0
- package/dist/useWheelScroll.js.map +1 -0
- package/dist/util.d.ts +1 -6
- package/dist/util.js +5 -34
- package/dist/util.js.map +1 -1
- package/dist/vendor/copyToClipboard.d.ts +1 -10
- package/dist/vendor/copyToClipboard.js +14 -109
- package/dist/vendor/copyToClipboard.js.map +1 -1
- package/dist/vendor/fileSaver.d.ts +1 -11
- package/dist/vendor/fileSaver.js +7 -76
- package/dist/vendor/fileSaver.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +10 -13
- package/src/collapseLogic.test.ts +115 -0
- package/src/components/Checkbox2.tsx +9 -18
- package/src/components/MSAViewer.tsx +67 -0
- package/src/components/SequenceTextArea.tsx +4 -4
- package/src/components/Track.tsx +10 -26
- package/src/components/dialogs/DomainDialog.tsx +4 -5
- package/src/components/dialogs/InterProScanDialog.tsx +7 -7
- package/src/components/dialogs/SettingsDialog.tsx +0 -37
- package/src/components/header/ColorSchemeMenu.tsx +35 -0
- package/src/components/header/FileMenu.tsx +84 -0
- package/src/components/header/Header.tsx +8 -6
- package/src/components/header/HeaderMenu.tsx +4 -155
- package/src/components/header/MSASettingsMenu.tsx +48 -0
- package/src/components/header/SettingsMenu.tsx +0 -23
- package/src/components/header/TreeSettingsMenu.tsx +96 -0
- package/src/components/header/ZoomMenu.tsx +0 -8
- package/src/components/header/getDomainsMenu.ts +83 -0
- package/src/components/import/ImportFormExamples.tsx +38 -35
- package/src/components/msa/MSACanvas.tsx +21 -91
- package/src/components/msa/MSACanvasBlock.tsx +1 -3
- package/src/components/msa/renderBoxFeatureCanvasBlock.ts +1 -1
- package/src/components/msa/renderMSABlock.ts +2 -5
- package/src/components/msa/renderMSAMouseover.ts +0 -6
- package/src/components/tree/TreeCanvas.tsx +35 -100
- package/src/components/tree/TreeNodeMenu.tsx +5 -14
- package/src/components/tree/renderTreeCanvas.test.ts +205 -0
- package/src/components/tree/renderTreeCanvas.ts +64 -27
- package/src/constants.ts +0 -2
- package/src/fetchUtils.ts +0 -5
- package/src/flatToTree.ts +20 -38
- package/src/hierarchy.test.ts +120 -0
- package/src/hierarchy.ts +221 -0
- package/src/index.ts +2 -0
- package/src/launchInterProScan.ts +4 -3
- package/src/model/DataModel.ts +12 -1
- package/src/model/msaModel.ts +0 -2
- package/src/model/treeModel.ts +2 -18
- package/src/model.ts +180 -278
- package/src/neighborJoining.ts +38 -628
- package/src/parseAsn1.test.ts +4 -1
- package/src/parseAsn1.ts +135 -405
- package/src/useWheelScroll.ts +109 -0
- package/src/util.ts +5 -50
- package/src/vendor/copyToClipboard.ts +14 -122
- package/src/vendor/fileSaver.ts +8 -105
- package/src/version.ts +1 -1
- package/dist/components/dialogs/AddTrackDialog.d.ts +0 -8
- package/dist/components/dialogs/AddTrackDialog.js +0 -30
- package/dist/components/dialogs/AddTrackDialog.js.map +0 -1
- package/dist/components/dialogs/TabPanel.d.ts +0 -6
- package/dist/components/dialogs/TabPanel.js +0 -6
- package/dist/components/dialogs/TabPanel.js.map +0 -1
- package/dist/components/header/ZoomStar.js +0 -40
- package/dist/components/header/ZoomStar.js.map +0 -1
- package/dist/layout.d.ts +0 -26
- package/dist/layout.js +0 -74
- package/dist/layout.js.map +0 -1
- package/dist/reparseTree.d.ts +0 -2
- package/dist/reparseTree.js +0 -15
- package/dist/reparseTree.js.map +0 -1
- package/src/components/dialogs/AddTrackDialog.tsx +0 -85
- package/src/components/dialogs/TabPanel.tsx +0 -19
- package/src/components/header/ZoomStar.tsx +0 -74
- package/src/createPaletteMap.test.ts +0 -57
- package/src/layout.ts +0 -118
- 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
|
-
|
|
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 =
|
|
56
|
-
const sx =
|
|
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
|
-
|
|
98
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
22
|
-
|
|
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
|
-
|
|
25
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
52
|
-
roots.push(node)
|
|
34
|
+
root ??= node
|
|
53
35
|
}
|
|
54
|
-
}
|
|
36
|
+
}
|
|
55
37
|
|
|
56
|
-
return
|
|
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
|
+
})
|
package/src/hierarchy.ts
ADDED
|
@@ -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
|
-
|
|
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
|
-
|
|
122
|
+
async function loadInterProScanResultsWithStatus({
|
|
122
123
|
jobId,
|
|
123
124
|
model,
|
|
124
125
|
}: {
|
package/src/model/DataModel.ts
CHANGED
|
@@ -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
|
}
|
package/src/model/msaModel.ts
CHANGED