react-msaview 5.7.3 → 5.8.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 (54) hide show
  1. package/bundle/index.js +91 -91
  2. package/bundle/index.js.map +4 -4
  3. package/dist/components/SequenceTextArea.js +2 -2
  4. package/dist/components/SequenceTextArea.js.map +1 -1
  5. package/dist/components/dialogs/UserProvidedDomainsDialog.js +6 -1
  6. package/dist/components/dialogs/UserProvidedDomainsDialog.js.map +1 -1
  7. package/dist/components/header/getDomainsMenu.js +1 -1
  8. package/dist/components/header/getDomainsMenu.js.map +1 -1
  9. package/dist/components/import/ImportForm.js +7 -11
  10. package/dist/components/import/ImportForm.js.map +1 -1
  11. package/dist/components/import/ImportFormExamples.js +7 -11
  12. package/dist/components/import/ImportFormExamples.js.map +1 -1
  13. package/dist/components/import/util.d.ts +1 -1
  14. package/dist/components/import/util.js +1 -1
  15. package/dist/components/import/util.js.map +1 -1
  16. package/dist/components/msa/renderBoxFeatureCanvasBlock.js +2 -2
  17. package/dist/components/msa/renderBoxFeatureCanvasBlock.js.map +1 -1
  18. package/dist/components/tree/renderTreeCanvas.d.ts +9 -9
  19. package/dist/components/tree/renderTreeCanvas.js +36 -24
  20. package/dist/components/tree/renderTreeCanvas.js.map +1 -1
  21. package/dist/flatToTree.d.ts +3 -1
  22. package/dist/flatToTree.js +16 -9
  23. package/dist/flatToTree.js.map +1 -1
  24. package/dist/hierarchy.d.ts +12 -17
  25. package/dist/hierarchy.js +12 -145
  26. package/dist/hierarchy.js.map +1 -1
  27. package/dist/model.d.ts +1 -0
  28. package/dist/model.js +17 -3
  29. package/dist/model.js.map +1 -1
  30. package/dist/neighborJoining.js +11 -2
  31. package/dist/neighborJoining.js.map +1 -1
  32. package/dist/version.d.ts +1 -1
  33. package/dist/version.js +1 -1
  34. package/package.json +4 -3
  35. package/src/colSpace.test.ts +40 -0
  36. package/src/components/SequenceTextArea.tsx +2 -2
  37. package/src/components/dialogs/UserProvidedDomainsDialog.tsx +8 -1
  38. package/src/components/header/getDomainsMenu.ts +1 -1
  39. package/src/components/import/ImportForm.tsx +6 -9
  40. package/src/components/import/ImportFormExamples.tsx +11 -14
  41. package/src/components/import/util.ts +1 -1
  42. package/src/components/msa/renderBoxFeatureCanvasBlock.ts +3 -2
  43. package/src/components/tree/renderTreeCanvas.test.ts +54 -1
  44. package/src/components/tree/renderTreeCanvas.ts +41 -28
  45. package/src/flatToTree.test.ts +21 -0
  46. package/src/flatToTree.ts +21 -10
  47. package/src/hierarchy.ts +44 -179
  48. package/src/largeTree.test.ts +32 -0
  49. package/src/model.ts +17 -3
  50. package/src/neighborJoining.test.ts +21 -6
  51. package/src/neighborJoining.ts +12 -2
  52. package/src/parseAsn1.test.ts +16 -0
  53. package/src/stripDefaultSnapshot.test.ts +19 -0
  54. package/src/version.ts +1 -1
@@ -0,0 +1,40 @@
1
+ // @vitest-environment jsdom
2
+ import { expect, test } from 'vitest'
3
+
4
+ import MSAModelF from './model.ts'
5
+
6
+ // a ragged alignment: row `a` stops early, so the alignment is as wide as `b`.
7
+ // Column space has to follow the widest row -- the gap analysis already does --
8
+ // or every column past the first row's end becomes unreachable: unscrollable,
9
+ // unhoverable, and missing from the minimap.
10
+ const msa = `>a
11
+ ACGT
12
+ >b
13
+ ACGTACGTAC`
14
+
15
+ function makeModel() {
16
+ const model = MSAModelF().create({
17
+ id: 'col-space-test',
18
+ type: 'MsaView',
19
+ msaFormat: 'fasta',
20
+ data: { msa },
21
+ })
22
+ model.setWidth(800)
23
+ return model
24
+ }
25
+
26
+ test('column space spans the widest row', () => {
27
+ const model = makeModel()
28
+ expect(model.numColumns).toBe(10)
29
+ expect(model.totalWidth).toBe(10 * model.colWidth)
30
+ })
31
+
32
+ test('the last column agrees with the column statistics', () => {
33
+ const model = makeModel()
34
+ expect(model.colStats.numColumns).toBe(model.numColumns)
35
+ expect(model.conservation).toHaveLength(model.numColumns)
36
+
37
+ model.setMousePos(model.numColumns - 1, 1)
38
+ expect(model.mouseOverColumnStats?.col).toBe(9)
39
+ expect(model.visibleColToRowLetter('b', 9)).toBe('C')
40
+ })
@@ -2,6 +2,7 @@ import React, { useState } from 'react'
2
2
 
3
3
  import { makeStyles } from '@jbrowse/core/util/tss-react'
4
4
  import { Button, TextField } from '@mui/material'
5
+ import { getUngappedSequence } from 'msa-parsers'
5
6
 
6
7
  import copy from '../vendor/copyToClipboard.ts'
7
8
  import Checkbox2 from './Checkbox2.tsx'
@@ -24,9 +25,8 @@ export default function SequenceTextArea({ str }: { str: [string, string][] }) {
24
25
  const [showGaps, setShowGaps] = useState(false)
25
26
  const [showEmpty, setShowEmpty] = useState(false)
26
27
 
27
- const removeGaps = (s: string) => s.replaceAll('-', '').replaceAll('.', '')
28
28
  const disp = str
29
- .map(([s1, s2]) => [s1, showGaps ? s2 : removeGaps(s2)] as const)
29
+ .map(([s1, s2]) => [s1, showGaps ? s2 : getUngappedSequence(s2)] as const)
30
30
  .filter(([, s2]) => showEmpty || !!s2)
31
31
  .map(([s1, s2]) => `>${s1}\n${s2}`)
32
32
  .join('\n')
@@ -112,8 +112,15 @@ const UserProvidedDomainsDialog = observer(function ({
112
112
  ? JSON.parse(await file.text())
113
113
  : await jsonfetch(interProURL)
114
114
 
115
+ // key each result by its xref id; a result without one has no
116
+ // row to attach to, and keying it `undefined` would draw a
117
+ // phantom row's worth of domains
115
118
  model.setDomains(
116
- Object.fromEntries(ret.results.map(r => [r.xref[0]!.id, r])),
119
+ Object.fromEntries(
120
+ ret.results
121
+ .map(r => [r.xref[0]?.id, r] as const)
122
+ .filter(e => e[0] !== undefined),
123
+ ),
117
124
  )
118
125
  getSession(model).notify(
119
126
  'Loaded interproscan results',
@@ -49,7 +49,7 @@ export function getDomainMenu({ model }: { model: MsaViewModel }) {
49
49
  label: `Show domains${noDomains ? ' (no domains loaded)' : ''}`,
50
50
  disabled: noDomains,
51
51
  icon: Visibility,
52
- checked: actuallyShowDomains ? showDomains : false,
52
+ checked: actuallyShowDomains,
53
53
  type: 'checkbox' as const,
54
54
  onClick: () => {
55
55
  model.setShowDomains(!showDomains)
@@ -57,15 +57,12 @@ const ImportForm = observer(function ({ model }: { model: MsaViewModel }) {
57
57
  <div>
58
58
  <Button
59
59
  onClick={() => {
60
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
61
- ;(async () => {
62
- try {
63
- await load(model, msaFile, treeFile, gffFile)
64
- } catch (e) {
65
- console.error(e)
66
- model.setError(e)
67
- }
68
- })()
60
+ try {
61
+ load(model, msaFile, treeFile, gffFile)
62
+ } catch (e) {
63
+ console.error(e)
64
+ model.setError(e)
65
+ }
69
66
  }}
70
67
  variant="contained"
71
68
  color="primary"
@@ -49,20 +49,17 @@ const ImportFormExamples = observer(function ({
49
49
  tree?: string
50
50
  gff?: string
51
51
  }) {
52
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
53
- ;(async () => {
54
- try {
55
- await load(
56
- model,
57
- msa ? { uri: msa, locationType: 'UriLocation' } : undefined,
58
- tree ? { uri: tree, locationType: 'UriLocation' } : undefined,
59
- gff ? { uri: gff, locationType: 'UriLocation' } : undefined,
60
- )
61
- } catch (e) {
62
- console.error(e)
63
- model.setError(e)
64
- }
65
- })()
52
+ try {
53
+ load(
54
+ model,
55
+ msa ? { uri: msa, locationType: 'UriLocation' } : undefined,
56
+ tree ? { uri: tree, locationType: 'UriLocation' } : undefined,
57
+ gff ? { uri: gff, locationType: 'UriLocation' } : undefined,
58
+ )
59
+ } catch (e) {
60
+ console.error(e)
61
+ model.setError(e)
62
+ }
66
63
  }
67
64
  return (
68
65
  <ul>
@@ -1,7 +1,7 @@
1
1
  import type { MsaViewModel } from '../../model.ts'
2
2
  import type { FileLocation } from '@jbrowse/core/util'
3
3
 
4
- export async function load(
4
+ export function load(
5
5
  model: MsaViewModel,
6
6
  msaFile?: FileLocation,
7
7
  treeFile?: FileLocation,
@@ -23,8 +23,9 @@ export function renderBoxFeatureCanvasBlock({
23
23
  blockSizeXOverride?: number
24
24
  blockSizeYOverride?: number
25
25
  }) {
26
- const { blockSize, rowHeight, highResScaleFactor, showDomains } = model
27
- if (showDomains) {
26
+ const { blockSize, rowHeight, highResScaleFactor, actuallyShowDomains } =
27
+ model
28
+ if (actuallyShowDomains) {
28
29
  const k = highResScaleFactorOverride ?? highResScaleFactor
29
30
  const bx = blockSizeXOverride ?? blockSize
30
31
  const by = blockSizeYOverride ?? blockSize
@@ -1,9 +1,12 @@
1
1
  import { describe, expect, it } from 'vitest'
2
2
 
3
3
  import { calcDepthToLeaf, findMaxBranchLen } from '../../hierarchy.ts'
4
- import { getNodeX } from './renderTreeCanvas.ts'
4
+ import stateModelFactory from '../../model.ts'
5
+ import { getNodeX, renderTreeCanvas } from './renderTreeCanvas.ts'
5
6
 
6
7
  import type { HierarchyNode } from '../../hierarchy.ts'
8
+ import type { RenderCtx } from '../renderCtx.ts'
9
+ import type { Theme } from '@mui/material'
7
10
 
8
11
  function leaf(id: string, len?: number): HierarchyNode {
9
12
  return {
@@ -113,3 +116,53 @@ describe('getNodeX cladogram positioning', () => {
113
116
  expect(getNodeX(leaf('a', 2.5), true, 100, 1)).toBe(2.5)
114
117
  })
115
118
  })
119
+
120
+ // the x values the tree pass actually strokes, for a model built from newick
121
+ function drawnTreeXs(newick: string) {
122
+ const model = stateModelFactory().create({
123
+ type: 'MsaView',
124
+ data: { msa: '>a\nA\n>b\nA\n>c\nA\n>d\nA', tree: newick },
125
+ })
126
+ model.setWidth(1000)
127
+ // labels would need a canvas to measure; the branch geometry is the subject
128
+ model.setDrawLabels(false)
129
+ model.setDrawNodeBubbles(false)
130
+
131
+ const xs: number[] = []
132
+ const ctx = {
133
+ font: '12px sans-serif',
134
+ beginPath() {},
135
+ stroke() {},
136
+ resetTransform() {},
137
+ scale() {},
138
+ translate() {},
139
+ moveTo(x: number) {
140
+ xs.push(x)
141
+ },
142
+ lineTo(x: number) {
143
+ xs.push(x)
144
+ },
145
+ } as unknown as RenderCtx
146
+
147
+ renderTreeCanvas({
148
+ model,
149
+ ctx,
150
+ offsetY: 0,
151
+ theme: { palette: { text: { primary: '#000' } } } as Theme,
152
+ })
153
+ return xs
154
+ }
155
+
156
+ describe('renderTreeCanvas horizontal extent', () => {
157
+ it('spreads a cladogram across the tree area when branch lengths are absent', () => {
158
+ // a lengthless newick forces cladogram mode; scaling it by the (zero) max
159
+ // branch length would stack every node on x=0 as one vertical line
160
+ const xs = drawnTreeXs('((a,b),(c,d));')
161
+ expect(Math.max(...xs)).toBe(300)
162
+ })
163
+
164
+ it('scales a phylogram by branch length', () => {
165
+ const xs = drawnTreeXs('((a:0.1,b:0.2):0.3,(c:0.4,d:0.5):0.6);')
166
+ expect(Math.max(...xs)).toBeCloseTo(300)
167
+ })
168
+ })
@@ -24,14 +24,15 @@ function inYBlock(y: number, offsetY: number, by: number) {
24
24
  return y > offsetY - extendBounds && y < offsetY + by + extendBounds
25
25
  }
26
26
 
27
- // Calculate node x-coordinate for both phylogram (with branch lengths) and cladogram (topology only) modes
28
- // For cladograms: x = (maxDepthToLeaf - nodeDepthToLeaf) / maxDepthToLeaf * maxWidth
29
- // This positions: leaves at maxWidth (rightmost), root at 0 (leftmost), internal nodes proportionally in between
27
+ // Calculate node x-coordinate for both phylogram (with branch lengths) and
28
+ // cladogram (topology only) modes. tipX is the pixel x the tips sit at.
29
+ // For cladograms: x = (maxDepthToLeaf - nodeDepthToLeaf) / maxDepthToLeaf * tipX
30
+ // This positions: leaves at tipX (rightmost), root at 0 (leftmost), internal nodes proportionally in between
30
31
  // Matches ape's: xx <- max(xx) - xx (where xx is depth from each node to tips)
31
32
  export function getNodeX(
32
33
  node: HierarchyNode,
33
34
  showBranchLen: boolean,
34
- maxBranchLen: number,
35
+ tipX: number,
35
36
  maxDepthToLeaf: number,
36
37
  ): number | undefined {
37
38
  if (showBranchLen) {
@@ -41,7 +42,7 @@ export function getNodeX(
41
42
  return 0
42
43
  }
43
44
  const depthToLeaf = calcDepthToLeaf(node)
44
- return ((maxDepthToLeaf - depthToLeaf) / maxDepthToLeaf) * maxBranchLen
45
+ return ((maxDepthToLeaf - depthToLeaf) / maxDepthToLeaf) * tipX
45
46
  }
46
47
 
47
48
  export function renderTree({
@@ -49,7 +50,7 @@ export function renderTree({
49
50
  ctx,
50
51
  model,
51
52
  theme,
52
- maxBranchLen,
53
+ tipX,
53
54
  maxDepthToLeaf,
54
55
  blockSizeYOverride,
55
56
  }: {
@@ -57,7 +58,7 @@ export function renderTree({
57
58
  ctx: RenderCtx
58
59
  model: MsaViewModel
59
60
  theme: Theme
60
- maxBranchLen: number
61
+ tipX: number
61
62
  maxDepthToLeaf: number
62
63
  blockSizeYOverride?: number
63
64
  }) {
@@ -67,8 +68,8 @@ export function renderTree({
67
68
  forEachLink(hierarchy, (source, target) => {
68
69
  const sy = source.x!
69
70
  const ty = target.x!
70
- const tx = getNodeX(target, showBranchLen, maxBranchLen, maxDepthToLeaf)
71
- const sx = getNodeX(source, showBranchLen, maxBranchLen, maxDepthToLeaf)
71
+ const tx = getNodeX(target, showBranchLen, tipX, maxDepthToLeaf)
72
+ const sx = getNodeX(source, showBranchLen, tipX, maxDepthToLeaf)
72
73
  if (tx === undefined || sx === undefined) {
73
74
  return
74
75
  }
@@ -94,7 +95,7 @@ export function renderCollapsedTriangles({
94
95
  offsetY,
95
96
  model,
96
97
  theme,
97
- maxBranchLen,
98
+ tipX,
98
99
  maxDepthToLeaf,
99
100
  blockSizeYOverride,
100
101
  }: {
@@ -103,7 +104,7 @@ export function renderCollapsedTriangles({
103
104
  offsetY: number
104
105
  model: MsaViewModel
105
106
  theme: Theme
106
- maxBranchLen: number
107
+ tipX: number
107
108
  maxDepthToLeaf: number
108
109
  blockSizeYOverride?: number
109
110
  }) {
@@ -116,19 +117,23 @@ export function renderCollapsedTriangles({
116
117
  fontSize,
117
118
  marginLeft: ml,
118
119
  } = model
120
+ // nothing collapsed is the common case, and the traversal below visits every
121
+ // node in the tree on every block of every redraw
122
+ if (collapsed.length === 0) {
123
+ return
124
+ }
125
+ const collapsedSet = new Set(collapsed)
119
126
  const by = blockSizeYOverride ?? blockSize
120
127
  const halfHeight = Math.max(2, rowHeight * 0.42)
121
128
  forEachDescendant(hierarchy, node => {
122
129
  const { id, name } = node.data
123
- if (collapsed.includes(id)) {
124
- const apexX = getNodeX(node, showBranchLen, maxBranchLen, maxDepthToLeaf)
130
+ if (collapsedSet.has(id)) {
131
+ const apexX = getNodeX(node, showBranchLen, tipX, maxDepthToLeaf)
125
132
  const y = node.x!
126
133
  const inBlock = inYBlock(y, offsetY, by)
127
134
  // in cladogram mode the hidden tips align at the right edge, otherwise use
128
135
  // the branch-length extent recorded in the model's hierarchy getter
129
- const baseX = showBranchLen
130
- ? (node.collapsedTipXFar ?? maxBranchLen)
131
- : maxBranchLen
136
+ const baseX = showBranchLen ? (node.collapsedTipXFar ?? tipX) : tipX
132
137
  if (apexX !== undefined && inBlock && baseX > apexX) {
133
138
  ctx.beginPath()
134
139
  ctx.moveTo(apexX, y)
@@ -171,7 +176,7 @@ export function renderNodeBubbles({
171
176
  clickMap,
172
177
  offsetY,
173
178
  model,
174
- maxBranchLen,
179
+ tipX,
175
180
  maxDepthToLeaf,
176
181
  blockSizeYOverride,
177
182
  }: {
@@ -179,7 +184,7 @@ export function renderNodeBubbles({
179
184
  clickMap?: ClickMapIndex
180
185
  offsetY: number
181
186
  model: MsaViewModel
182
- maxBranchLen: number
187
+ tipX: number
183
188
  maxDepthToLeaf: number
184
189
  blockSizeYOverride?: number
185
190
  }) {
@@ -191,8 +196,9 @@ export function renderNodeBubbles({
191
196
  marginLeft: ml,
192
197
  } = model
193
198
  const by = blockSizeYOverride ?? blockSize
199
+ const collapsedSet = new Set(collapsed)
194
200
  forEachDescendant(hierarchy, node => {
195
- const x = getNodeX(node, showBranchLen, maxBranchLen, maxDepthToLeaf)
201
+ const x = getNodeX(node, showBranchLen, tipX, maxDepthToLeaf)
196
202
  if (x === undefined) {
197
203
  return
198
204
  }
@@ -200,7 +206,7 @@ export function renderNodeBubbles({
200
206
  const y = node.x!
201
207
  const { id, name } = data
202
208
  if (node.height >= 1 && inYBlock(y, offsetY, by)) {
203
- const isCollapsed = collapsed.includes(id)
209
+ const isCollapsed = collapsedSet.has(id)
204
210
  ctx.strokeStyle = 'black'
205
211
  ctx.fillStyle = isCollapsed ? 'black' : 'white'
206
212
  ctx.beginPath()
@@ -231,7 +237,7 @@ export function renderTreeLabels({
231
237
  offsetY,
232
238
  ctx,
233
239
  clickMap,
234
- maxBranchLen,
240
+ tipX,
235
241
  maxDepthToLeaf,
236
242
  blockSizeYOverride,
237
243
  }: {
@@ -240,7 +246,7 @@ export function renderTreeLabels({
240
246
  ctx: RenderCtx
241
247
  clickMap?: ClickMapIndex
242
248
  theme: Theme
243
- maxBranchLen: number
249
+ tipX: number
244
250
  maxDepthToLeaf: number
245
251
  blockSizeYOverride?: number
246
252
  }) {
@@ -289,7 +295,7 @@ export function renderTreeLabels({
289
295
  const yp = y + fontSize / 4
290
296
  let xp = 0
291
297
  if (!noTree) {
292
- xp = getNodeX(node, showBranchLen, maxBranchLen, maxDepthToLeaf) ?? 0
298
+ xp = getNodeX(node, showBranchLen, tipX, maxDepthToLeaf) ?? 0
293
299
  }
294
300
 
295
301
  const width =
@@ -375,7 +381,14 @@ export function renderTreeCanvas({
375
381
 
376
382
  // memoized on the model and shared across the tree/bubble/label passes (and
377
383
  // across all tree blocks) rather than re-traversing the hierarchy in each
378
- const { maxBranchLength: maxBranchLen, maxDepthToLeaf } = model
384
+ const { maxBranchLength, maxDepthToLeaf, showBranchLenEffective, treeWidth } =
385
+ model
386
+ // pixel x of the tips. A phylogram scales branch lengths so the longest
387
+ // root-to-tip path lands at maxBranchLength; a cladogram ignores lengths and
388
+ // spreads topological depth across the whole tree area instead. Reusing
389
+ // maxBranchLength for the cladogram would pin every node to x=0 for a tree
390
+ // with no branch lengths -- which is exactly what forces cladogram mode.
391
+ const tipX = showBranchLenEffective ? maxBranchLength : treeWidth
379
392
 
380
393
  if (!noTree && drawTree) {
381
394
  renderTree({
@@ -383,7 +396,7 @@ export function renderTreeCanvas({
383
396
  offsetY,
384
397
  model,
385
398
  theme,
386
- maxBranchLen,
399
+ tipX,
387
400
  maxDepthToLeaf,
388
401
  blockSizeYOverride,
389
402
  })
@@ -394,7 +407,7 @@ export function renderTreeCanvas({
394
407
  offsetY,
395
408
  model,
396
409
  theme,
397
- maxBranchLen,
410
+ tipX,
398
411
  maxDepthToLeaf,
399
412
  blockSizeYOverride,
400
413
  })
@@ -405,7 +418,7 @@ export function renderTreeCanvas({
405
418
  offsetY,
406
419
  clickMap,
407
420
  model,
408
- maxBranchLen,
421
+ tipX,
409
422
  maxDepthToLeaf,
410
423
  blockSizeYOverride,
411
424
  })
@@ -419,7 +432,7 @@ export function renderTreeCanvas({
419
432
  model,
420
433
  clickMap,
421
434
  theme,
422
- maxBranchLen,
435
+ tipX,
423
436
  maxDepthToLeaf,
424
437
  blockSizeYOverride,
425
438
  })
@@ -15,6 +15,27 @@ describe('flatToTree', () => {
15
15
  expect(root.children[0]!.children.map(c => c.id)).toEqual(['3'])
16
16
  })
17
17
 
18
+ test('carries the label and branch length of a BioTreeContainer node', () => {
19
+ const root = flatToTree([
20
+ { id: 0, label: 'Multiple organisms' },
21
+ {
22
+ id: 1,
23
+ parent: 0,
24
+ label: 'sodium/glucose cotransporter 4',
25
+ dist: '0.5',
26
+ },
27
+ { id: 2, parent: 0 },
28
+ ])
29
+ expect(root.name).toBe('Multiple organisms')
30
+ expect(root.children[0]).toMatchObject({
31
+ name: 'sodium/glucose cotransporter 4',
32
+ length: 0.5,
33
+ })
34
+ // no label falls back to the id, and no dist leaves the length unset
35
+ expect(root.children[1]!.name).toBe('2')
36
+ expect(root.children[1]!.length).toBeUndefined()
37
+ })
38
+
18
39
  test('throws on empty input rather than returning undefined', () => {
19
40
  expect(() => flatToTree([])).toThrow(/no root/)
20
41
  })
package/src/flatToTree.ts CHANGED
@@ -1,26 +1,37 @@
1
1
  interface FlatItem {
2
2
  id: number
3
3
  parent?: number
4
+ // NCBI BioTreeContainer features, already un-remapped by parseAsn1. Both are
5
+ // strings there: the ASN.1 encodes every feature value as one.
6
+ label?: string
7
+ dist?: string
4
8
  }
5
9
 
6
10
  interface TreeNode {
7
11
  id: string
8
12
  name: string
9
- parent?: string
13
+ length?: number
10
14
  children: TreeNode[]
11
15
  }
12
16
 
13
17
  export function flatToTree(items: FlatItem[]): TreeNode {
14
18
  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
- ]),
19
+ items.map(item => {
20
+ const length = item.dist === undefined ? undefined : Number(item.dist)
21
+ return [
22
+ item.id,
23
+ {
24
+ id: `${item.id}`,
25
+ // the label is what NCBI's own tree viewer shows; falling back to the
26
+ // node id would render a tree of bare numbers
27
+ name: item.label || `${item.id}`,
28
+ ...(length !== undefined && Number.isFinite(length)
29
+ ? { length }
30
+ : {}),
31
+ children: [] as TreeNode[],
32
+ },
33
+ ]
34
+ }),
24
35
  )
25
36
 
26
37
  let root: TreeNode | undefined