react-msaview 5.1.1 → 5.2.1

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 (190) hide show
  1. package/README.md +41 -8
  2. package/bundle/index.js +17 -17
  3. package/bundle/index.js.LICENSE.txt +1 -1
  4. package/bundle/index.js.map +1 -1
  5. package/dist/calculateBlocks.d.ts +4 -10
  6. package/dist/calculateBlocks.js +12 -20
  7. package/dist/calculateBlocks.js.map +1 -1
  8. package/dist/components/ConservationTrack.js +20 -37
  9. package/dist/components/ConservationTrack.js.map +1 -1
  10. package/dist/components/MSAView.js +8 -1
  11. package/dist/components/MSAView.js.map +1 -1
  12. package/dist/components/ResizeHandles.d.ts +3 -0
  13. package/dist/components/ResizeHandles.js +60 -105
  14. package/dist/components/ResizeHandles.js.map +1 -1
  15. package/dist/components/TextTrack.js +25 -34
  16. package/dist/components/TextTrack.js.map +1 -1
  17. package/dist/components/Track.js +1 -1
  18. package/dist/components/Track.js.map +1 -1
  19. package/dist/components/VerticalScrollbar.js +2 -2
  20. package/dist/components/VerticalScrollbar.js.map +1 -1
  21. package/dist/components/dialogs/ExportSVGDialog.js +20 -20
  22. package/dist/components/dialogs/ExportSVGDialog.js.map +1 -1
  23. package/dist/components/dialogs/InterProScanDialog.js +6 -1
  24. package/dist/components/dialogs/InterProScanDialog.js.map +1 -1
  25. package/dist/components/dialogs/UserProvidedDomainsDialog.js +1 -2
  26. package/dist/components/dialogs/UserProvidedDomainsDialog.js.map +1 -1
  27. package/dist/components/header/ColorSchemeMenu.js +1 -1
  28. package/dist/components/header/ColorSchemeMenu.js.map +1 -1
  29. package/dist/components/header/FileMenu.js +3 -2
  30. package/dist/components/header/FileMenu.js.map +1 -1
  31. package/dist/components/header/Header.js +1 -1
  32. package/dist/components/header/HeaderInfoArea.js +28 -8
  33. package/dist/components/header/HeaderInfoArea.js.map +1 -1
  34. package/dist/components/header/MSASettingsMenu.js +1 -1
  35. package/dist/components/header/MSASettingsMenu.js.map +1 -1
  36. package/dist/components/header/TreeSettingsMenu.js +1 -1
  37. package/dist/components/header/TreeSettingsMenu.js.map +1 -1
  38. package/dist/components/header/ZoomControls.js +7 -2
  39. package/dist/components/header/ZoomControls.js.map +1 -1
  40. package/dist/components/header/ZoomMenu.js +9 -0
  41. package/dist/components/header/ZoomMenu.js.map +1 -1
  42. package/dist/components/header/settingsMenuItems.js +24 -8
  43. package/dist/components/header/settingsMenuItems.js.map +1 -1
  44. package/dist/components/minimap/Minimap.js +6 -17
  45. package/dist/components/minimap/Minimap.js.map +1 -1
  46. package/dist/components/minimap/MinimapSVG.js +7 -17
  47. package/dist/components/minimap/MinimapSVG.js.map +1 -1
  48. package/dist/components/minimap/minimapLayout.d.ts +9 -0
  49. package/dist/components/minimap/minimapLayout.js +15 -0
  50. package/dist/components/minimap/minimapLayout.js.map +1 -0
  51. package/dist/components/msa/MSACanvas.js +6 -1
  52. package/dist/components/msa/MSACanvas.js.map +1 -1
  53. package/dist/components/msa/MSACanvasBlock.js +5 -1
  54. package/dist/components/msa/MSACanvasBlock.js.map +1 -1
  55. package/dist/components/msa/MSAMouseoverCanvas.js +2 -2
  56. package/dist/components/msa/MSAMouseoverCanvas.js.map +1 -1
  57. package/dist/components/msa/getVisibleLeaves.d.ts +6 -0
  58. package/dist/components/msa/getVisibleLeaves.js +9 -0
  59. package/dist/components/msa/getVisibleLeaves.js.map +1 -0
  60. package/dist/components/msa/renderBoxFeatureCanvasBlock.d.ts +2 -1
  61. package/dist/components/msa/renderBoxFeatureCanvasBlock.js +5 -6
  62. package/dist/components/msa/renderBoxFeatureCanvasBlock.js.map +1 -1
  63. package/dist/components/msa/renderMSABlock.d.ts +2 -1
  64. package/dist/components/msa/renderMSABlock.js +100 -88
  65. package/dist/components/msa/renderMSABlock.js.map +1 -1
  66. package/dist/components/msa/renderMSAMouseover.js +5 -4
  67. package/dist/components/msa/renderMSAMouseover.js.map +1 -1
  68. package/dist/components/msa/visibleColRange.d.ts +8 -0
  69. package/dist/components/msa/visibleColRange.js +10 -0
  70. package/dist/components/msa/visibleColRange.js.map +1 -0
  71. package/dist/components/renderCtx.d.ts +7 -0
  72. package/dist/components/renderCtx.js +2 -0
  73. package/dist/components/renderCtx.js.map +1 -0
  74. package/dist/components/tracks/renderTracksSvg.d.ts +6 -5
  75. package/dist/components/tracks/renderTracksSvg.js +13 -5
  76. package/dist/components/tracks/renderTracksSvg.js.map +1 -1
  77. package/dist/components/tree/TreeCanvas.js.map +1 -1
  78. package/dist/components/tree/TreeCanvasBlock.js +2 -10
  79. package/dist/components/tree/TreeCanvasBlock.js.map +1 -1
  80. package/dist/components/tree/renderTreeCanvas.d.ts +5 -4
  81. package/dist/components/tree/renderTreeCanvas.js +18 -26
  82. package/dist/components/tree/renderTreeCanvas.js.map +1 -1
  83. package/dist/components/util.d.ts +0 -14
  84. package/dist/components/util.js +0 -58
  85. package/dist/components/util.js.map +1 -1
  86. package/dist/constants.d.ts +4 -0
  87. package/dist/constants.js +7 -0
  88. package/dist/constants.js.map +1 -1
  89. package/dist/flatToTree.js +6 -0
  90. package/dist/flatToTree.js.map +1 -1
  91. package/dist/hierarchy.d.ts +3 -1
  92. package/dist/hierarchy.js +142 -89
  93. package/dist/hierarchy.js.map +1 -1
  94. package/dist/index.d.ts +1 -0
  95. package/dist/index.js +8 -0
  96. package/dist/index.js.map +1 -1
  97. package/dist/launchInterProScan.js +22 -9
  98. package/dist/launchInterProScan.js.map +1 -1
  99. package/dist/measureTextCanvas.js +2 -1
  100. package/dist/measureTextCanvas.js.map +1 -1
  101. package/dist/model.d.ts +234 -176
  102. package/dist/model.js +285 -259
  103. package/dist/model.js.map +1 -1
  104. package/dist/parseAsn1.js +8 -2
  105. package/dist/parseAsn1.js.map +1 -1
  106. package/dist/renderToSvg.d.ts +1 -1
  107. package/dist/renderToSvg.js +43 -46
  108. package/dist/renderToSvg.js.map +1 -1
  109. package/dist/rowCoordinateCalculations.d.ts +0 -9
  110. package/dist/rowCoordinateCalculations.js +0 -20
  111. package/dist/rowCoordinateCalculations.js.map +1 -1
  112. package/dist/setFontSize.d.ts +3 -0
  113. package/dist/setFontSize.js +14 -0
  114. package/dist/setFontSize.js.map +1 -0
  115. package/dist/useDevicePixelRatio.d.ts +1 -0
  116. package/dist/useDevicePixelRatio.js +27 -0
  117. package/dist/useDevicePixelRatio.js.map +1 -0
  118. package/dist/useWheelScroll.d.ts +3 -1
  119. package/dist/useWheelScroll.js +64 -22
  120. package/dist/useWheelScroll.js.map +1 -1
  121. package/dist/util.d.ts +5 -0
  122. package/dist/util.js +59 -1
  123. package/dist/util.js.map +1 -1
  124. package/dist/version.d.ts +1 -1
  125. package/dist/version.js +1 -1
  126. package/dist/webpack.js +0 -1
  127. package/dist/webpack.js.map +1 -1
  128. package/dist/wheelZoom.d.ts +4 -0
  129. package/dist/wheelZoom.js +43 -0
  130. package/dist/wheelZoom.js.map +1 -0
  131. package/package.json +5 -5
  132. package/src/calculateBlocks.ts +18 -51
  133. package/src/components/ConservationTrack.tsx +20 -45
  134. package/src/components/MSAView.tsx +10 -1
  135. package/src/components/ResizeHandles.tsx +101 -113
  136. package/src/components/TextTrack.tsx +31 -38
  137. package/src/components/Track.tsx +1 -1
  138. package/src/components/VerticalScrollbar.tsx +2 -2
  139. package/src/components/dialogs/ExportSVGDialog.tsx +27 -17
  140. package/src/components/dialogs/InterProScanDialog.tsx +6 -1
  141. package/src/components/dialogs/UserProvidedDomainsDialog.tsx +1 -2
  142. package/src/components/header/ColorSchemeMenu.tsx +1 -0
  143. package/src/components/header/FileMenu.tsx +3 -1
  144. package/src/components/header/Header.tsx +1 -1
  145. package/src/components/header/HeaderInfoArea.tsx +32 -7
  146. package/src/components/header/MSASettingsMenu.tsx +1 -0
  147. package/src/components/header/TreeSettingsMenu.tsx +1 -0
  148. package/src/components/header/ZoomControls.tsx +14 -1
  149. package/src/components/header/ZoomMenu.tsx +10 -0
  150. package/src/components/header/settingsMenuItems.ts +24 -8
  151. package/src/components/minimap/Minimap.tsx +6 -20
  152. package/src/components/minimap/MinimapSVG.tsx +10 -27
  153. package/src/components/minimap/minimapLayout.ts +17 -0
  154. package/src/components/msa/MSACanvas.tsx +9 -0
  155. package/src/components/msa/MSACanvasBlock.tsx +7 -1
  156. package/src/components/msa/MSAMouseoverCanvas.tsx +4 -3
  157. package/src/components/msa/getVisibleLeaves.ts +21 -0
  158. package/src/components/msa/renderBoxFeatureCanvasBlock.ts +9 -9
  159. package/src/components/msa/renderMSABlock.ts +123 -125
  160. package/src/components/msa/renderMSAMouseover.ts +5 -4
  161. package/src/components/msa/visibleColRange.ts +17 -0
  162. package/src/components/renderCtx.ts +29 -0
  163. package/src/components/tracks/renderTracksSvg.ts +20 -10
  164. package/src/components/tree/TreeCanvas.tsx +3 -1
  165. package/src/components/tree/TreeCanvasBlock.tsx +2 -12
  166. package/src/components/tree/renderTreeCanvas.ts +27 -30
  167. package/src/components/util.ts +0 -81
  168. package/src/constants.ts +8 -0
  169. package/src/flatToTree.test.ts +30 -0
  170. package/src/flatToTree.ts +10 -1
  171. package/src/hierarchy.test.ts +53 -1
  172. package/src/hierarchy.ts +153 -100
  173. package/src/index.ts +8 -0
  174. package/src/launchInterProScan.ts +33 -11
  175. package/src/measureTextCanvas.ts +3 -1
  176. package/src/model.ts +359 -306
  177. package/src/parseAsn1.test.ts +6 -0
  178. package/src/parseAsn1.ts +11 -3
  179. package/src/renderToSvg.tsx +99 -95
  180. package/src/rowCoordinateCalculations.test.ts +0 -51
  181. package/src/rowCoordinateCalculations.ts +0 -23
  182. package/src/setFontSize.test.ts +33 -0
  183. package/src/setFontSize.ts +20 -0
  184. package/src/useDevicePixelRatio.ts +29 -0
  185. package/src/useWheelScroll.ts +79 -21
  186. package/src/util.ts +60 -1
  187. package/src/version.ts +1 -1
  188. package/src/webpack.ts +0 -1
  189. package/src/wheelZoom.test.ts +46 -0
  190. package/src/wheelZoom.ts +47 -0
@@ -1,7 +1,13 @@
1
- import { calcDepthToLeaf, descendants, links } from '../../hierarchy.ts'
1
+ import {
2
+ calcDepthToLeaf,
3
+ forEachDescendant,
4
+ forEachLink,
5
+ } from '../../hierarchy.ts'
6
+ import { setFontSize } from '../../setFontSize.ts'
2
7
 
3
8
  import type { HierarchyNode } from '../../hierarchy.ts'
4
9
  import type { MsaViewModel } from '../../model.ts'
10
+ import type { RenderCtx } from '../renderCtx.ts'
5
11
  import type { Theme } from '@mui/material'
6
12
 
7
13
  export const padding = 600
@@ -59,7 +65,7 @@ export function renderTree({
59
65
  blockSizeYOverride,
60
66
  }: {
61
67
  offsetY: number
62
- ctx: CanvasRenderingContext2D
68
+ ctx: RenderCtx
63
69
  model: MsaViewModel
64
70
  theme: Theme
65
71
  maxBranchLen: number
@@ -67,16 +73,15 @@ export function renderTree({
67
73
  blockSizeYOverride?: number
68
74
  }) {
69
75
  const { hierarchy, showBranchLenEffective: showBranchLen, blockSize } = model
70
- const by = blockSizeYOverride || blockSize
76
+ const by = blockSizeYOverride ?? blockSize
71
77
  ctx.strokeStyle = theme.palette.text.primary
72
- for (const link of links(hierarchy)) {
73
- const { source, target } = link
78
+ forEachLink(hierarchy, (source, target) => {
74
79
  const sy = source.x!
75
80
  const ty = target.x!
76
81
  const tx = getNodeX(target, showBranchLen, maxBranchLen, maxDepthToLeaf)
77
82
  const sx = getNodeX(source, showBranchLen, maxBranchLen, maxDepthToLeaf)
78
83
  if (tx === undefined || sx === undefined) {
79
- continue
84
+ return
80
85
  }
81
86
 
82
87
  const y1 = Math.min(sy, ty)
@@ -91,7 +96,7 @@ export function renderTree({
91
96
  ctx.lineTo(tx, ty)
92
97
  ctx.stroke()
93
98
  }
94
- }
99
+ })
95
100
  }
96
101
 
97
102
  export function renderNodeBubbles({
@@ -103,7 +108,7 @@ export function renderNodeBubbles({
103
108
  maxDepthToLeaf,
104
109
  blockSizeYOverride,
105
110
  }: {
106
- ctx: CanvasRenderingContext2D
111
+ ctx: RenderCtx
107
112
  clickMap?: ClickMapIndex
108
113
  offsetY: number
109
114
  model: MsaViewModel
@@ -118,11 +123,11 @@ export function renderNodeBubbles({
118
123
  blockSize,
119
124
  marginLeft: ml,
120
125
  } = model
121
- const by = blockSizeYOverride || blockSize
122
- for (const node of descendants(hierarchy)) {
126
+ const by = blockSizeYOverride ?? blockSize
127
+ forEachDescendant(hierarchy, node => {
123
128
  const x = getNodeX(node, showBranchLen, maxBranchLen, maxDepthToLeaf)
124
129
  if (x === undefined) {
125
- continue
130
+ return
126
131
  }
127
132
  const { data } = node
128
133
  const y = node.x!
@@ -149,7 +154,7 @@ export function renderNodeBubbles({
149
154
  name,
150
155
  })
151
156
  }
152
- }
157
+ })
153
158
  }
154
159
 
155
160
  export function renderTreeLabels({
@@ -164,7 +169,7 @@ export function renderTreeLabels({
164
169
  }: {
165
170
  model: MsaViewModel
166
171
  offsetY: number
167
- ctx: CanvasRenderingContext2D
172
+ ctx: RenderCtx
168
173
  clickMap?: ClickMapIndex
169
174
  theme: Theme
170
175
  maxBranchLen: number
@@ -183,8 +188,9 @@ export function renderTreeLabels({
183
188
  marginLeft,
184
189
  leaves,
185
190
  noTree,
191
+ labelWidthMap,
186
192
  } = model
187
- const by = blockSizeYOverride || blockSize
193
+ const by = blockSizeYOverride ?? blockSize
188
194
  const emHeight = ctx.measureText('M').width
189
195
  if (labelsAlignRight) {
190
196
  ctx.textAlign = 'right'
@@ -204,10 +210,11 @@ export function renderTreeLabels({
204
210
  const yp = y + fontSize / 4
205
211
  let xp = 0
206
212
  if (!noTree) {
207
- xp = getNodeX(node, showBranchLen, maxBranchLen, maxDepthToLeaf) || 0
213
+ xp = getNodeX(node, showBranchLen, maxBranchLen, maxDepthToLeaf) ?? 0
208
214
  }
209
215
 
210
- const { width } = ctx.measureText(displayName)
216
+ const width =
217
+ labelWidthMap.get(name) ?? ctx.measureText(displayName).width
211
218
 
212
219
  ctx.fillStyle = theme.palette.text.primary
213
220
  if (labelsAlignRight) {
@@ -255,7 +262,7 @@ export function renderTreeCanvas({
255
262
  }: {
256
263
  model: MsaViewModel
257
264
  offsetY: number
258
- ctx: CanvasRenderingContext2D
265
+ ctx: RenderCtx
259
266
  clickMap?: ClickMapIndex
260
267
  theme: Theme
261
268
  highResScaleFactorOverride?: number
@@ -272,27 +279,17 @@ export function renderTreeCanvas({
272
279
  fontSize,
273
280
  showTreeText,
274
281
  marginLeft,
275
- nref,
276
282
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
277
- rowHeight: _rowHeight, // this is needed for redrawing after zoom change
283
+ rowHeight: _rowHeight, // read so the draw autorun re-fires after a zoom change
278
284
  } = model
279
285
 
280
286
  ctx.resetTransform()
281
287
 
282
- // this is a bogus use of nref, it is never less than 0. we just are using
283
- // it in this statement because otherwise it would be an unused variable,
284
- // we just need to use nref to indicate a redraw in an autorun when canvas
285
- // ref is updated and in order to convince bundlers like not to delete
286
- // unused usage with propertyReadSideEffects
287
- const k =
288
- nref < 0
289
- ? Number.NEGATIVE_INFINITY
290
- : highResScaleFactorOverride || highResScaleFactor
288
+ const k = highResScaleFactorOverride ?? highResScaleFactor
291
289
  ctx.scale(k, k)
292
290
  ctx.translate(marginLeft, -offsetY)
293
291
 
294
- const font = ctx.font
295
- ctx.font = font.replace(/\d+px/, `${fontSize}px`)
292
+ setFontSize(ctx, fontSize)
296
293
 
297
294
  // memoized on the model and shared across the tree/bubble/label passes (and
298
295
  // across all tree blocks) rather than re-traversing the hierarchy in each
@@ -1,84 +1,3 @@
1
- /**
2
- * Given a scale ( bp/px ) and minimum distances (px) between major and minor
3
- * gridlines, return an object like `{ majorPitch: bp, minorPitch: bp }` giving
4
- * the gridline pitches to use.
5
- */
6
- export function chooseGridPitch(
7
- scale: number,
8
- minMajorPitchPx: number,
9
- minMinorPitchPx: number,
10
- ) {
11
- scale = Math.abs(scale)
12
- const minMajorPitchBp = minMajorPitchPx * scale
13
- const majorMagnitude = Number.parseInt(
14
- minMajorPitchBp.toExponential().split(/e/i)[1]!,
15
- 10,
16
- )
17
-
18
- let majorPitch = 10 ** majorMagnitude
19
- while (majorPitch < minMajorPitchBp) {
20
- majorPitch *= 2
21
- if (majorPitch >= minMajorPitchBp) {
22
- break
23
- }
24
- majorPitch *= 2.5
25
- }
26
-
27
- majorPitch = Math.max(majorPitch, 5)
28
-
29
- const majorPitchPx = majorPitch / scale
30
-
31
- let minorPitch = 0
32
- if (!(majorPitch % 10) && majorPitchPx / 10 >= minMinorPitchPx) {
33
- minorPitch = majorPitch / 10
34
- } else if (!(majorPitch % 5) && majorPitchPx / 5 >= minMinorPitchPx) {
35
- minorPitch = majorPitch / 5
36
- } else if (!(majorPitch % 2) && majorPitchPx / 2 >= minMinorPitchPx) {
37
- minorPitch = majorPitch / 2
38
- }
39
-
40
- return { majorPitch, minorPitch }
41
- }
42
- export function makeTicks(
43
- start: number,
44
- end: number,
45
- bpPerPx: number,
46
- emitMajor = true,
47
- emitMinor = true,
48
- ) {
49
- const gridPitch = chooseGridPitch(bpPerPx, 60, 15)
50
-
51
- let minBase = start
52
- let maxBase = end
53
-
54
- if (bpPerPx < 0) {
55
- ;[minBase, maxBase] = [maxBase, minBase]
56
- }
57
-
58
- // add 20px additional on the right and left to allow us to draw the ends of
59
- // labels that lie a little outside our region
60
- minBase -= Math.abs(20 * bpPerPx)
61
- maxBase += Math.abs(20 * bpPerPx) + 1
62
-
63
- const iterPitch = gridPitch.minorPitch || gridPitch.majorPitch
64
- let index = 0
65
- const ticks = []
66
- for (
67
- let base = Math.ceil(minBase / iterPitch) * iterPitch;
68
- base < maxBase;
69
- base += iterPitch
70
- ) {
71
- if (emitMinor && base % (gridPitch.majorPitch * 2)) {
72
- ticks.push({ type: 'minor', base: base - 1, index })
73
- index += 1
74
- } else if (emitMajor && !(base % (gridPitch.majorPitch * 2))) {
75
- ticks.push({ type: 'major', base: base - 1, index })
76
- index += 1
77
- }
78
- }
79
- return ticks
80
- }
81
-
82
1
  export function mathPower(num: number): string {
83
2
  if (num < 999) {
84
3
  return String(num)
package/src/constants.ts CHANGED
@@ -10,6 +10,14 @@ export const defaultHideGaps = true
10
10
  export const defaultAllowedGappyness = 100
11
11
  export const defaultSubFeatureRows = false
12
12
  export const defaultDrawMsaLetters = true
13
+ export const defaultScrollZoom = false
14
+
15
+ // Zoom limits, px per cell. maxCellSize caps both smooth zoom and the stepwise
16
+ // zoom-in buttons (which otherwise grow unbounded). minColWidth/minRowHeight are
17
+ // the smooth-zoom floors.
18
+ export const minColWidth = 0.2
19
+ export const minRowHeight = 1
20
+ export const maxCellSize = 80
13
21
 
14
22
  // MSA model defaults
15
23
  export const defaultBgColor = true
@@ -0,0 +1,30 @@
1
+ import { describe, expect, test } from 'vitest'
2
+
3
+ import { flatToTree } from './flatToTree.ts'
4
+
5
+ describe('flatToTree', () => {
6
+ test('builds a tree from a flat parent list', () => {
7
+ const root = flatToTree([
8
+ { id: 0 },
9
+ { id: 1, parent: 0 },
10
+ { id: 2, parent: 0 },
11
+ { id: 3, parent: 1 },
12
+ ])
13
+ expect(root.id).toBe('0')
14
+ expect(root.children.map(c => c.id)).toEqual(['1', '2'])
15
+ expect(root.children[0]!.children.map(c => c.id)).toEqual(['3'])
16
+ })
17
+
18
+ test('throws on empty input rather than returning undefined', () => {
19
+ expect(() => flatToTree([])).toThrow(/no root/)
20
+ })
21
+
22
+ test('throws when every node has a parent (cycle) rather than returning undefined', () => {
23
+ expect(() =>
24
+ flatToTree([
25
+ { id: 0, parent: 1 },
26
+ { id: 1, parent: 0 },
27
+ ]),
28
+ ).toThrow(/no root/)
29
+ })
30
+ })
package/src/flatToTree.ts CHANGED
@@ -35,5 +35,14 @@ export function flatToTree(items: FlatItem[]): TreeNode {
35
35
  }
36
36
  }
37
37
 
38
- return root!
38
+ if (root === undefined) {
39
+ // no node lacks a (resolvable) parent: empty input, or every node's parent
40
+ // points back into the set (a cycle). returning undefined here would crash
41
+ // cryptically downstream, so fail with a diagnosable message instead
42
+ throw new Error(
43
+ 'could not build tree from flat node list: no root found (input was empty or contains a parent cycle)',
44
+ )
45
+ }
46
+
47
+ return root
39
48
  }
@@ -1,9 +1,36 @@
1
1
  import { describe, expect, test } from 'vitest'
2
2
 
3
- import { collapse, find, hierarchy, leaves, sort, sum } from './hierarchy.ts'
3
+ import {
4
+ calcDepthToLeaf,
5
+ collapse,
6
+ find,
7
+ findMaxBranchLen,
8
+ hierarchy,
9
+ leaves,
10
+ maxLength,
11
+ setBrLength,
12
+ sort,
13
+ sum,
14
+ } from './hierarchy.ts'
4
15
 
5
16
  import type { NodeWithIds } from './types.ts'
6
17
 
18
+ // Builds a fully unbalanced "caterpillar" tree of the given depth: each internal
19
+ // node has one leaf child and one internal child. Recursion over this would blow
20
+ // the call stack; the iterative traversals must handle it.
21
+ function makeDeepTree(depth: number): NodeWithIds {
22
+ let node: NodeWithIds = { id: 'leaf0', name: 'leaf0', children: [] }
23
+ for (let i = 1; i <= depth; i++) {
24
+ node = {
25
+ id: `node${i}`,
26
+ name: `node${i}`,
27
+ length: 1,
28
+ children: [{ id: `leaf${i}`, name: `leaf${i}`, children: [] }, node],
29
+ }
30
+ }
31
+ return node
32
+ }
33
+
7
34
  function makeTree(): NodeWithIds {
8
35
  return {
9
36
  id: 'root',
@@ -118,3 +145,28 @@ describe('sum', () => {
118
145
  expect(find(h, n => n.data.id === 'A')?.value).toBe(2)
119
146
  })
120
147
  })
148
+
149
+ describe('deep (caterpillar) trees do not overflow the stack', () => {
150
+ const depth = 100_000
151
+
152
+ test('build, height, leaf count, and ordering', () => {
153
+ const h = hierarchy(makeDeepTree(depth), d => d.children)
154
+ expect(h.height).toBe(depth)
155
+ const l = leaves(h)
156
+ expect(l.length).toBe(depth + 1)
157
+ // each level contributes its own leafN before descending; deepest leaf last
158
+ expect(l[0]!.data.name).toBe(`leaf${depth}`)
159
+ expect(l.at(-1)!.data.name).toBe('leaf0')
160
+ })
161
+
162
+ test('accumulation helpers (sum, find, maxLength, setBrLength, depth)', () => {
163
+ const h = hierarchy(makeDeepTree(depth), d => d.children)
164
+ sum(h, d => (d.children.length > 0 ? 0 : 1))
165
+ expect(h.value).toBe(depth + 1)
166
+ expect(find(h, n => n.data.id === 'leaf0')?.data.name).toBe('leaf0')
167
+ expect(maxLength(h)).toBe(depth)
168
+ expect(calcDepthToLeaf(h)).toBe(depth)
169
+ setBrLength(h, 0, 1)
170
+ expect(findMaxBranchLen(h)).toBe(depth)
171
+ })
172
+ })