@figtreejs/core 0.0.1-alpha.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 (111) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/eslint.config.js +9 -0
  3. package/package.json +76 -0
  4. package/src/@custom-types/normalize-svg-path.d.ts +13 -0
  5. package/src/@custom-types/parse-svg-path.d.ts +8 -0
  6. package/src/@custom-types/svg-path-types.d.ts +37 -0
  7. package/src/bauble-makers/makers.ts +112 -0
  8. package/src/bauble-makers/set-up-baubles.ts +197 -0
  9. package/src/bauble-makers/utils.ts +61 -0
  10. package/src/components/baubles/bauble.tsx +61 -0
  11. package/src/components/baubles/branches.tsx +13 -0
  12. package/src/components/baubles/clades/cartoon.tsx +68 -0
  13. package/src/components/baubles/clades/highlight.tsx +96 -0
  14. package/src/components/baubles/clades/index.ts +1 -0
  15. package/src/components/baubles/clades.tsx +45 -0
  16. package/src/components/baubles/helpers.tsx +62 -0
  17. package/src/components/baubles/index.ts +16 -0
  18. package/src/components/baubles/labels.tsx +38 -0
  19. package/src/components/baubles/nodes.tsx +51 -0
  20. package/src/components/baubles/shapes/branch.tsx +53 -0
  21. package/src/components/baubles/shapes/circle.tsx +64 -0
  22. package/src/components/baubles/shapes/index.ts +9 -0
  23. package/src/components/baubles/shapes/label.tsx +104 -0
  24. package/src/components/baubles/shapes/rectangle.tsx +83 -0
  25. package/src/components/baubles/types.ts +99 -0
  26. package/src/components/decorations/axis/axis-types.ts +123 -0
  27. package/src/components/decorations/axis/axis.tsx +21 -0
  28. package/src/components/decorations/axis/index.ts +2 -0
  29. package/src/components/decorations/axis/polar-axis-bars.tsx +102 -0
  30. package/src/components/decorations/axis/polar-axis.tsx +175 -0
  31. package/src/components/decorations/axis/rectangular-axis-bars.tsx +53 -0
  32. package/src/components/decorations/axis/rectangular-axis.tsx +151 -0
  33. package/src/components/decorations/index.ts +2 -0
  34. package/src/components/decorations/legend/discrete-legend.tsx +93 -0
  35. package/src/components/decorations/legend/index.ts +1 -0
  36. package/src/components/decorations/legend/legend.tsx +1 -0
  37. package/src/components/figtree/figtree-types.ts +69 -0
  38. package/src/components/figtree/figtree.tsx +136 -0
  39. package/src/components/figtree/index.ts +3 -0
  40. package/src/components/hoc/index.ts +7 -0
  41. package/src/components/hoc/with-branch.tsx +148 -0
  42. package/src/components/hoc/with-branches.tsx +54 -0
  43. package/src/components/hoc/with-clades.tsx +47 -0
  44. package/src/components/hoc/with-node.tsx +183 -0
  45. package/src/components/hoc/with-nodes.tsx +45 -0
  46. package/src/components/index.ts +4 -0
  47. package/src/context/aminated-context.ts +3 -0
  48. package/src/context/dimension-context.ts +22 -0
  49. package/src/context/layout-context.ts +20 -0
  50. package/src/context/scale-context.ts +12 -0
  51. package/src/evo/index.ts +1 -0
  52. package/src/evo/tree/index.ts +5 -0
  53. package/src/evo/tree/mcc-tree.ts +0 -0
  54. package/src/evo/tree/normalized-tree/immutable-tree-helpers.ts +136 -0
  55. package/src/evo/tree/normalized-tree/immutable-tree.test.ts +158 -0
  56. package/src/evo/tree/normalized-tree/immutable-tree.ts +1365 -0
  57. package/src/evo/tree/normalized-tree/index.ts +3 -0
  58. package/src/evo/tree/parsers/annotation-parser.ts +276 -0
  59. package/src/evo/tree/parsers/index.ts +3 -0
  60. package/src/evo/tree/parsers/newick-character-parser.ts +246 -0
  61. package/src/evo/tree/parsers/newick-parsing.ts +22 -0
  62. package/src/evo/tree/parsers/nexus-parser.ts +12 -0
  63. package/src/evo/tree/parsers/nexus-parsing.ts +68 -0
  64. package/src/evo/tree/parsers/parsing.test.ts +289 -0
  65. package/src/evo/tree/parsers/stream-reader/index.ts +1 -0
  66. package/src/evo/tree/parsers/stream-reader/newick-importer.txt +395 -0
  67. package/src/evo/tree/parsers/stream-reader/nexus-importer.test.ts +99 -0
  68. package/src/evo/tree/parsers/stream-reader/nexus-importer.ts +293 -0
  69. package/src/evo/tree/parsers/stream-reader/nexus-tokenizer.ts +77 -0
  70. package/src/evo/tree/parsers/stream-reader/nexus-transform-stream.txt +109 -0
  71. package/src/evo/tree/taxa/helper-functions.ts +46 -0
  72. package/src/evo/tree/taxa/index.ts +1 -0
  73. package/src/evo/tree/taxa/taxon.ts +116 -0
  74. package/src/evo/tree/traversals/index.ts +1 -0
  75. package/src/evo/tree/traversals/preorder-traversal.ts +89 -0
  76. package/src/evo/tree/traversals/traversal-types.ts +6 -0
  77. package/src/evo/tree/tree-types.ts +197 -0
  78. package/src/evo/tree/utilities.ts +44 -0
  79. package/src/index.ts +6 -0
  80. package/src/layouts/functional/index.ts +2 -0
  81. package/src/layouts/functional/radial-layout.ts +150 -0
  82. package/src/layouts/functional/rectangular-layout.ts +71 -0
  83. package/src/layouts/index.ts +3 -0
  84. package/src/layouts/layout-interface.ts +90 -0
  85. package/src/layouts/types.ts +32 -0
  86. package/src/path.helpers.ts +81 -0
  87. package/src/store/polar-scale.ts +145 -0
  88. package/src/store/store.ts +144 -0
  89. package/src/tests/baubles/__snapshots__/branch-labels.test.tsx.snap +901 -0
  90. package/src/tests/baubles/__snapshots__/node-labels.test.tsx.snap +1516 -0
  91. package/src/tests/baubles/branch-labels.test.tsx +103 -0
  92. package/src/tests/baubles/label.svg +131 -0
  93. package/src/tests/baubles/node-labels.test.tsx +126 -0
  94. package/src/tests/clades/__snapshots__/cartoon.test.tsx.snap +327 -0
  95. package/src/tests/clades/__snapshots__/highlight.test.tsx.snap +337 -0
  96. package/src/tests/clades/cartoon.test.tsx +65 -0
  97. package/src/tests/clades/highlight.test.tsx +66 -0
  98. package/src/tests/figtree/__snapshots__/figtree.test.tsx.snap +761 -0
  99. package/src/tests/figtree/figtree.test.tsx +123 -0
  100. package/src/tests/figtree/simple.svg +47 -0
  101. package/src/tests/layouts/radiallayout.test.ts +23 -0
  102. package/src/tests/layouts/rectangularlayout.test.ts +65 -0
  103. package/src/tests/shapes/branch.test.tsx +40 -0
  104. package/src/tests/shapes/circle.test.tsx +47 -0
  105. package/src/tests/shapes/label.test.tsx +101 -0
  106. package/src/tests/shapes/rectangle.test.tsx +67 -0
  107. package/src/tests/shapes/types.ts +1 -0
  108. package/src/utils.ts +57 -0
  109. package/tsconfig.json +12 -0
  110. package/vite.config.ts +34 -0
  111. package/vitetest.config.ts +11 -0
@@ -0,0 +1,47 @@
1
+ /**
2
+ * An HOC that calculates the clades needed for clade shapes and
3
+ * passes them on to the shapes themselves
4
+ */
5
+
6
+ import type { NodeRef } from "../../evo";
7
+ import type { AttrsRecord, InternalInteractionType } from "../baubles/types";
8
+
9
+ export type Clade = {
10
+ root: NodeRef;
11
+ leftMost: NodeRef;
12
+ rightMost: NodeRef;
13
+ mostDiverged: NodeRef;
14
+ };
15
+
16
+ export type CladeProps<A extends object> = {
17
+ clades: Clade[];
18
+ attrs?: AttrsRecord<A>;
19
+ interactions?: { [key: string]: InternalInteractionType }; // keyed by node id // check type
20
+ // shape:NodeShapes,
21
+ keyBy?: (n: NodeRef) => string;
22
+ };
23
+ export function withClades<T extends object>(
24
+ ShapeComponent: React.FC<T & { clade: Clade }>,
25
+ ): React.FC<CladeProps<T>> {
26
+ const withClades = (props: CladeProps<T>) => {
27
+ const { clades, keyBy = (n: NodeRef) => n._id, attrs = {} } = props;
28
+
29
+ return (
30
+ <g className={"node-layer"}>
31
+ {clades.map((clade) => {
32
+ const cladeAttrs = attrs[clade.root._id] ?? {};
33
+ return (
34
+ <ShapeComponent
35
+ key={keyBy(clade.root)}
36
+ clade={clade}
37
+ {...cladeAttrs}
38
+ // interactions={nodeInteractions}
39
+ />
40
+ );
41
+ })}
42
+ </g>
43
+ );
44
+ };
45
+ withClades.displayName = `withCladessArray(${ShapeComponent.displayName || ShapeComponent.name || "Component"})`;
46
+ return withClades;
47
+ }
@@ -0,0 +1,183 @@
1
+ import React, { useContext } from "react";
2
+ import type { NodeRef } from "../../evo";
3
+ import { ScaleContext } from "../../context/scale-context";
4
+ import { layoutContext } from "../../context/layout-context";
5
+ import { animatedContext } from "../../context/aminated-context";
6
+ import { DimensionContext } from "../../context/dimension-context";
7
+ import { layoutClass } from "../../layouts";
8
+ import { defaultNodeLabelData } from "../../store/store";
9
+ import type { PolarVertex } from "../../layouts/types";
10
+ import { textSafeDegrees } from "../../store/polar-scale";
11
+ import { unNullify } from "../../utils";
12
+
13
+ //The goal here is now to take a shape components that accepts Attrs: number | string , x/y
14
+ // and return a component that takes a node / layout/ scale and attrs:number|string | function
15
+
16
+ type Injected = {
17
+ x: number;
18
+ y: number;
19
+ animated: boolean;
20
+ };
21
+
22
+ /**
23
+ * This HOC takes a shape (possibly animated) that requires x,y values and calculated attributes and
24
+ * calculates those values from a node.
25
+ *
26
+ */
27
+
28
+ export function withNode<T extends object>(
29
+ WrappedComponent: React.FC<T & Injected>,
30
+ ) {
31
+ type ExposedProps = T & { node: NodeRef };
32
+
33
+ const NodedComponent: React.FC<ExposedProps> = (props) => {
34
+ const scale = useContext(ScaleContext);
35
+ const layout = useContext(layoutContext);
36
+ const animated = useContext(animatedContext);
37
+
38
+ const { node, ...rest } = props;
39
+ const v = scale(layout(node));
40
+
41
+ return (
42
+ <WrappedComponent {...(rest as T)} x={v.x} y={v.y} animated={animated} />
43
+ );
44
+ };
45
+
46
+ NodedComponent.displayName = `withNode(${WrappedComponent.displayName || WrappedComponent.name || "Component"})`;
47
+
48
+ return NodedComponent;
49
+ }
50
+
51
+ /**
52
+ * An HOC that determines the x,y, rotation alignment base and text anchor of a label
53
+ * if a parent is passed in then the component renders a branch label
54
+ */
55
+ export type LabelInjection = {
56
+ x: number;
57
+ y: number;
58
+ animated: boolean;
59
+ alignmentBaseline: React.ComponentProps<"text">["alignmentBaseline"];
60
+ rotation: number;
61
+ textAnchor: React.ComponentProps<"text">["textAnchor"];
62
+ d?: string;
63
+ };
64
+ export function withNodeLabel<T extends object>(
65
+ WrappedComponent: React.FC<T & LabelInjection>,
66
+ ) {
67
+ // parent for branch
68
+ type ExposedProps = T & {
69
+ node: NodeRef;
70
+ parent?: NodeRef;
71
+ aligned?: boolean;
72
+ gap?: number;
73
+ };
74
+
75
+ const NodedComponent: React.FC<ExposedProps> = (props) => {
76
+ const scale = useContext(ScaleContext);
77
+ const layout = useContext(layoutContext);
78
+ const animated = useContext(animatedContext);
79
+ const { domainX, layoutClass: layoutType } = useContext(DimensionContext);
80
+
81
+ const { node, parent, aligned, gap = 6, ...rest } = props;
82
+ const v = layout(node);
83
+ const scaledV = scale(v);
84
+
85
+ if (parent === undefined) {
86
+ // node label
87
+ const nodeLabel = scaledV.nodeLabel ?? defaultNodeLabelData;
88
+ const dx = nodeLabel.dxFactor * gap;
89
+ const dy = nodeLabel.dyFactor * gap;
90
+
91
+ const scaledMax = scale({ x: domainX[1], y: v.y });
92
+
93
+ const xpos = (aligned ? scaledMax.x : scaledV.x) + dx;
94
+ const ypos =
95
+ (aligned && layoutType === layoutClass.Polar
96
+ ? scaledMax.y
97
+ : scaledV.y) + dy;
98
+ const { alignmentBaseline, rotation, textAnchor } = nodeLabel;
99
+
100
+ const d = aligned
101
+ ? `M${scaledV.x} ${scaledV.y}L${xpos} ${ypos}`
102
+ : `M${scaledV.x} ${scaledV.y}L${scaledV.x} ${scaledV.y}`;
103
+ return (
104
+ <WrappedComponent
105
+ alignmentBaseline={alignmentBaseline}
106
+ rotation={rotation}
107
+ textAnchor={textAnchor}
108
+ d={d}
109
+ x={xpos}
110
+ y={ypos}
111
+ {...(rest as T)}
112
+ animated={animated}
113
+ />
114
+ );
115
+ } else {
116
+ const parentVertex = layout(parent);
117
+ const scaledPv = scale(parentVertex);
118
+ // todo fix this so we don't need all the casting etc.
119
+ const theta =
120
+ layoutType === layoutClass.Polar
121
+ ? unNullify(
122
+ (scaledV as PolarVertex).theta,
123
+ "The layout is polar but theta was not calculated for this node",
124
+ )
125
+ : 0;
126
+ const rotation =
127
+ layoutType === layoutClass.Polar ? textSafeDegrees(theta) : 0;
128
+ const step = scale({ x: parentVertex.x, y: v.y });
129
+ const { dx, dy } =
130
+ layoutType === layoutClass.Polar
131
+ ? getPolarBranchDs(theta, gap)
132
+ : { dx: 0, dy: -1 * gap };
133
+ const x =
134
+ (layoutType === layoutClass.Polar
135
+ ? (scaledV.x + step.x) / 2
136
+ : (scaledV.x + scaledPv.x) / 2) + dx;
137
+ const y =
138
+ (layoutType === layoutClass.Polar
139
+ ? (scaledV.y + step.y) / 2
140
+ : layoutType === layoutClass.Radial
141
+ ? (scaledV.y + scaledPv.y) / 2
142
+ : scaledV.y) + dy;
143
+
144
+ return (
145
+ <WrappedComponent
146
+ alignmentBaseline={"baseline"}
147
+ rotation={rotation}
148
+ textAnchor={"middle"}
149
+ x={x}
150
+ y={y}
151
+ {...(rest as T)}
152
+ animated={animated}
153
+ />
154
+ );
155
+ }
156
+ };
157
+
158
+ NodedComponent.displayName = `withNodeLabel(${WrappedComponent.displayName || WrappedComponent.name || "Component"})`;
159
+
160
+ return NodedComponent;
161
+ }
162
+
163
+ function getPolarBranchDs(theta: number, gap: number) {
164
+ //branch lable dx dy;
165
+ let branchDx, branchDy;
166
+ if (theta > 0 && theta < Math.PI / 2) {
167
+ //good
168
+ branchDx = Math.sin(Math.PI / 2 - theta) * gap;
169
+ branchDy = -Math.cos(Math.PI / 2 - theta) * gap;
170
+ } else if (theta > Math.PI / 2 && theta < Math.PI) {
171
+ //good
172
+ branchDx = -Math.cos(Math.PI / 2 - (Math.PI - theta)) * gap;
173
+ branchDy = -Math.sin(Math.PI / 2 - (Math.PI - theta)) * gap;
174
+ } else if (theta > Math.PI && theta < (3 * Math.PI) / 2) {
175
+ // good
176
+ branchDx = Math.cos(Math.PI / 2 - (theta - Math.PI)) * gap;
177
+ branchDy = -Math.sin(Math.PI / 2 - (theta - Math.PI)) * gap;
178
+ } else {
179
+ branchDx = -Math.cos(Math.PI / 2 - (2 * Math.PI - theta)) * gap;
180
+ branchDy = -Math.sin(Math.PI / 2 - (2 * Math.PI - theta)) * gap;
181
+ }
182
+ return { dx: branchDx, dy: branchDy };
183
+ }
@@ -0,0 +1,45 @@
1
+ import type { NodeRef } from "../../evo";
2
+ import type { AttrsRecord, InternalInteractionType } from "../baubles/types";
3
+
4
+ export type NodeProps<A extends object> = {
5
+ nodes: NodeRef[];
6
+ attrs?: AttrsRecord<A>;
7
+ interactions?: { [key: string]: InternalInteractionType }; // keyed by node id // check type
8
+ aligned?: boolean;
9
+ keyBy?: (n: NodeRef) => string;
10
+ };
11
+
12
+ //TODO key by not being passed down
13
+
14
+ /**
15
+ * A component that
16
+ * The factory accepts the options above and returns a Bauble to be rendered by the figure.
17
+ */
18
+ //TODO do interactions
19
+ export function withNodes<T extends object>(
20
+ ShapeComponent: React.FC<T & { node: NodeRef }>,
21
+ ): React.FC<NodeProps<T>> {
22
+ const withNodes = (props: NodeProps<T>) => {
23
+ const { nodes, keyBy = (n: NodeRef) => n._id, attrs = {}, aligned } = props;
24
+
25
+ return (
26
+ <g className={"node-layer"}>
27
+ {nodes.map((node) => {
28
+ const nodeAttrs = attrs[node._id] ?? {};
29
+ // const nodeInteractions = interactions[node._id]?? {}
30
+ return (
31
+ <ShapeComponent
32
+ key={keyBy(node)}
33
+ node={node}
34
+ {...nodeAttrs}
35
+ aligned={aligned}
36
+ // interactions={nodeInteractions}
37
+ />
38
+ );
39
+ })}
40
+ </g>
41
+ );
42
+ };
43
+ withNodes.displayName = `withNodesArray(${ShapeComponent.displayName || ShapeComponent.name || "Component"})`;
44
+ return withNodes;
45
+ }
@@ -0,0 +1,4 @@
1
+ export * from "./baubles";
2
+ export * from "./figtree";
3
+ // export * from "./Tanglegram"
4
+ export * from "./decorations";
@@ -0,0 +1,3 @@
1
+ import { createContext } from "react";
2
+
3
+ export const animatedContext = createContext(false);
@@ -0,0 +1,22 @@
1
+ import { createContext } from "react";
2
+ import type { dimensionType } from "../components/figtree/figtree-types";
3
+ import { layoutClass } from "../layouts";
4
+
5
+ const defaultDimension: dimensionType = {
6
+ canvasWidth: 0,
7
+ canvasHeight: 0,
8
+ domainX: [0, 1],
9
+ domainY: [0, 1],
10
+ layoutClass: layoutClass.Rectangular,
11
+ invert: false,
12
+ pollard: 0,
13
+ minRadius: 0,
14
+ fishEye: {
15
+ x: 0,
16
+ y: 0,
17
+ scale: 0,
18
+ },
19
+ rootAngle: 0,
20
+ angleRange: 0,
21
+ };
22
+ export const DimensionContext = createContext(defaultDimension);
@@ -0,0 +1,20 @@
1
+ import { createContext } from "react";
2
+ import type { layoutType } from "../store/store";
3
+ import { layoutClass } from "../layouts";
4
+
5
+ const defaultLayout: layoutType = () => {
6
+ return {
7
+ x: 0,
8
+ y: 0,
9
+ layoutClass: layoutClass.Rectangular,
10
+ nodeLabel: {
11
+ // todo move this to a helper function
12
+ alignmentBaseline: "middle",
13
+ textAnchor: "end",
14
+ dxFactor: 0,
15
+ dyFactor: 0,
16
+ rotation: 0,
17
+ },
18
+ };
19
+ };
20
+ export const layoutContext = createContext(defaultLayout);
@@ -0,0 +1,12 @@
1
+ import { createContext } from "react";
2
+ import type { scaleType } from "../store/store";
3
+ import type { simpleVertex } from "../layouts/types";
4
+
5
+ const defaultScale: scaleType = <T extends simpleVertex>(vertex: T) => {
6
+ return {
7
+ ...vertex,
8
+ x: 0,
9
+ y: 0,
10
+ };
11
+ };
12
+ export const ScaleContext = createContext(defaultScale);
@@ -0,0 +1 @@
1
+ export * from "./tree";
@@ -0,0 +1,5 @@
1
+ export * from "./normalized-tree";
2
+ export * from "./tree-types";
3
+ export * from "./parsers/stream-reader";
4
+ export * from "./traversals";
5
+ export * from "./taxa";
File without changes
@@ -0,0 +1,136 @@
1
+ /**
2
+ * These function help access tree data while
3
+ * handling missing data.
4
+ *
5
+ * They cannot be private functions on the method because
6
+ * those can't be called by immer proxies.
7
+ */
8
+
9
+ import type { Maybe, Undefinable } from "@figtreejs/maybe/maybe";
10
+ import { MaybeType, Nothing, Some } from "@figtreejs/maybe/maybe";
11
+ import type { Annotation, NodeRef } from "../tree-types";
12
+ import type { ImmutableTree, nodeIndex, Node } from "./immutable-tree";
13
+ import type { Taxon } from "../taxa";
14
+ import {
15
+ maybeGetNameFromIndex,
16
+ maybeGetTaxonByName,
17
+ } from "../taxa/helper-functions";
18
+
19
+ /**
20
+ *
21
+ * A private getting for retrieving a NodeRef from a number
22
+ */
23
+ export function maybeGetNodeFromNumber(
24
+ tree: ImmutableTree,
25
+ i: number,
26
+ ): Maybe<NodeRef> {
27
+ const n = tree._data.nodes.allNodes[i] as Undefinable<NodeRef>;
28
+ if (n === undefined) {
29
+ return Nothing();
30
+ } else {
31
+ return Some(n);
32
+ }
33
+ }
34
+
35
+ export function maybeGetNode(
36
+ tree: ImmutableTree,
37
+ i: nodeIndex,
38
+ ): Maybe<NodeRef> {
39
+ if (typeof i === "number") {
40
+ return maybeGetNodeFromNumber(tree, i);
41
+ } else if (i instanceof Object) {
42
+ return maybeGetNodeByTaxon(tree, i);
43
+ } else if (typeof i === "string") {
44
+ const taxon = maybeGetTaxonByName(tree.taxonSet._data, i);
45
+ if (taxon.type === MaybeType.Some) {
46
+ return maybeGetNodeByTaxon(tree, taxon.value);
47
+ } else {
48
+ return maybeGetNodeByLabel(tree, i);
49
+ }
50
+ }
51
+ return Nothing();
52
+ }
53
+
54
+ export function maybeGetNodeByTaxon(
55
+ tree: ImmutableTree,
56
+ taxon: Taxon,
57
+ ): Maybe<NodeRef> {
58
+ const n = tree._data.nodes.byTaxon[taxon.number] as Undefinable<number>;
59
+ if (n === undefined) {
60
+ return Nothing();
61
+ } else {
62
+ return maybeGetNode(tree, n);
63
+ }
64
+ }
65
+
66
+ // export function maybeGetTaxonByName(tree:ImmutableTree, name:string):Maybe<Taxon>{
67
+ // const t = tree.taxonSet.getTaxonByName(name)
68
+ // if(t===undefined){
69
+ // return Nothing()
70
+ // }else{
71
+ // return Some(t)
72
+ // }
73
+ // }
74
+
75
+ export function maybeGetNodeByLabel(
76
+ tree: ImmutableTree,
77
+ label: string,
78
+ ): Maybe<NodeRef> {
79
+ const index = tree._data.nodes.byLabel[label] as Undefinable<number>;
80
+ if (index === undefined) {
81
+ return Nothing();
82
+ }
83
+ return maybeGetNodeFromNumber(tree, index);
84
+ }
85
+
86
+ export function maybeGetAnnotation(
87
+ tree: ImmutableTree,
88
+ node: NodeRef,
89
+ name: string,
90
+ ): Maybe<Annotation> {
91
+ const a = (tree.getNode(node.number) as Node).annotations[
92
+ name
93
+ ] as Undefinable<Annotation>;
94
+ if (a === undefined) {
95
+ return Nothing();
96
+ } else {
97
+ return Some(a);
98
+ }
99
+ }
100
+
101
+ export function maybeGetTaxonFromNode(
102
+ tree: ImmutableTree,
103
+ node: NodeRef,
104
+ ): Maybe<Taxon> {
105
+ const taxaIndex = tree._data.nodeToTaxon[node.number] as Undefinable<number>;
106
+ if (taxaIndex === undefined) {
107
+ return Nothing();
108
+ }
109
+ return maybeGetTaxon(tree, taxaIndex);
110
+ }
111
+
112
+ export function maybeGetTaxon(
113
+ tree: ImmutableTree,
114
+ id: number | NodeRef | string,
115
+ ): Maybe<Taxon> {
116
+ if (typeof id === "number") {
117
+ return maybeGetTaxonByName(
118
+ tree.taxonSet._data,
119
+ maybeGetNameFromIndex(tree.taxonSet._data, id),
120
+ );
121
+ } else if (typeof id === "string") {
122
+ return maybeGetTaxonByName(tree.taxonSet._data, id);
123
+ } else {
124
+ return maybeGetTaxonFromNode(tree, id);
125
+ }
126
+ }
127
+
128
+ export function maybeGetParent(
129
+ tree: ImmutableTree,
130
+ node: NodeRef,
131
+ ): Maybe<NodeRef> {
132
+ const n = tree._data.nodes.allNodes[node.number] as Undefinable<Node>;
133
+ if (n === undefined) return Nothing();
134
+ const parentId = n.parent;
135
+ return parentId === undefined ? Nothing() : maybeGetNode(tree, parentId);
136
+ }
@@ -0,0 +1,158 @@
1
+ //TODO test immutable tree include tests that check nodes to roots update as well.
2
+
3
+ import { notNull, u } from "../../../utils";
4
+ import { ImmutableTree } from "./immutable-tree";
5
+ import { describe, it, expect } from "vitest";
6
+
7
+ describe("ImmutableTree", () => {
8
+ it("simpleTree", function () {
9
+ const tree = new ImmutableTree();
10
+ const added = tree.addNodes();
11
+ const tree1 = added.tree;
12
+ expect(tree.getNodeCount()).toEqual(1);
13
+ expect(tree1.getNodeCount()).toEqual(2);
14
+ expect(tree1).not.toEqual(tree);
15
+ });
16
+ //Add child used getNode and updates both node and child.
17
+ it("build tree and check parent", function () {
18
+ const prototree = new ImmutableTree();
19
+ const {
20
+ tree: tree,
21
+ nodes: [child, child2],
22
+ } = prototree.addNodes(2);
23
+ notNull(child, "can not be null");
24
+ notNull(child2, "can not be null");
25
+ const parent = tree.getNode(0);
26
+ const tree1 = tree.addChild(parent, child).addChild(parent, child2);
27
+
28
+ expect(tree1).not.toEqual(tree);
29
+ expect(tree1.getChildCount(parent)).toEqual(2);
30
+
31
+ expect(tree1.getParent(child)).not.toEqual(parent); //bc parent changed when added child
32
+ expect(tree1.getParent(child)).toEqual(tree1.getNode(0)); //bc parent changed when added child
33
+ expect(() => tree.getParent(child)).toThrow("No parent for node 1");
34
+ expect(tree1.getChild(parent, 0)).toEqual(tree1.getNode(1));
35
+ expect(tree1.getChild(parent, 0)).not.toEqual(child); // this child has no parent!
36
+ });
37
+ it("Change branchlength", function () {
38
+ const prototree = new ImmutableTree();
39
+
40
+ const {
41
+ tree: tree,
42
+ nodes: [parent, child, child2],
43
+ } = prototree.addNodes(3);
44
+ notNull(child, "can not be null");
45
+ notNull(child2, "can not be null");
46
+ notNull(parent, "can not be null");
47
+
48
+ const tree1 = tree.addChild(parent, child).addChild(parent, child2);
49
+
50
+ const tree2 = tree1.setLength(child, 0.5);
51
+ expect(tree2).not.toEqual(tree1);
52
+ // expect(tree1.getNode(0)).not.toBe(tree2.getNode(0));
53
+ // expect(tree1.getParent(child)).not.toBe(tree2.getParent(child));
54
+ });
55
+ it("change height", function () {
56
+ const tree = ImmutableTree.fromNewick("((A:1,B:1):1,C:2);");
57
+ const A = tree.getTaxonByName("A");
58
+ const B = tree.getTaxonByName("B");
59
+ const nodeA = tree.getNodeByTaxon(A);
60
+
61
+ expect(tree.getHeight(nodeA)).toBe(0);
62
+ const tree1 = tree.setHeight(nodeA, 0.5);
63
+
64
+ const nodeA1 = tree1.getNodeByTaxon(A);
65
+ expect(tree1.getHeight(nodeA1)).toBe(0.5);
66
+
67
+ expect(
68
+ tree1.getHeight(u(tree1.getNodeByTaxon(u(tree.getTaxonByName("B"))))),
69
+ ).toBe(0);
70
+ expect(tree1.getLength(nodeA1)).toBe(0.5);
71
+
72
+ expect(tree1.getDivergence(nodeA1)).toBe(1.5);
73
+ expect(tree1.getDivergence(u(tree1.getNodeByTaxon(B)))).toBe(2);
74
+ });
75
+ it("order", function () {
76
+ const newickString = `((((((virus1:0.1,virus2:0.12):0.08,(virus3:0.011,virus4:0.0087):0.15):0.03,virus5:0.21):0.2,(virus6:0.45,virus7:0.4):0.02):0.1,virus8:0.4):0.1,(virus9:0.04,virus10:0.03):0.6);`;
77
+
78
+ const tree = ImmutableTree.fromNewick(newickString, {
79
+ parseAnnotations: false,
80
+ });
81
+ const orderedTree = tree.orderNodesByDensity(true);
82
+
83
+ expect(orderedTree.toNewick()).toBe(
84
+ `((virus9:0.04,virus10:0.03):0.6,(virus8:0.4,((virus6:0.45,virus7:0.4):0.02,(virus5:0.21,((virus1:0.1,virus2:0.12):0.08,(virus3:0.011,virus4:0.0087):0.15):0.03):0.2):0.1):0.1);`,
85
+ );
86
+ });
87
+
88
+ it("bigger reroot - caused issues once", function () {
89
+ const newickString = `((((((virus1:0.1,virus2:0.12):0.08,(virus3:0.011,virus4:0.0087):0.15):0.03,virus5:0.21):0.2,(virus6:0.45,virus7:0.4):0.02):0.1,virus8:0.4):0.1,(virus9:0.04,virus10:0.03):0.6);`;
90
+
91
+ const tree = ImmutableTree.fromNewick(newickString);
92
+ const virus3 = tree.getTaxonByName("virus3");
93
+ const tree1 = tree.reroot(
94
+ u(tree.getParent(u(tree.getNodeByTaxon(virus3)))),
95
+ 0.5,
96
+ );
97
+ expect(tree1.toNewick()).toBe(
98
+ "(((virus1:0.1,virus2:0.12):0.08,(virus5:0.21,((virus6:0.45,virus7:0.4):0.02,(virus8:0.4,(virus9:0.04,virus10:0.03):0.7):0.1):0.2):0.03):0.075,(virus3:0.011,virus4:0.0087):0.075);",
99
+ );
100
+ });
101
+
102
+ it("rotate", function () {
103
+ const tree = ImmutableTree.fromNewick("((A:1,B:1):1,C:2);");
104
+ const A = tree.getTaxonByName("A");
105
+ const B = tree.getTaxonByName("B");
106
+ const node = u(tree.getParent(u(tree.getNodeByTaxon(A))));
107
+ const child1 = tree.getChild(node, 0);
108
+ const rotatedTree = tree.rotate(node);
109
+ const child2 = rotatedTree.getChild(node, 0);
110
+
111
+ expect(rotatedTree.getTaxonFromNode(child2)).toBe(B);
112
+ expect(tree.getTaxonFromNode(child1)).toBe(A);
113
+ });
114
+ it("set height test", function () {
115
+ const tree = ImmutableTree.fromNewick("((A:1,B:1):1,C:2);");
116
+ const A = tree.getTaxonByName("A");
117
+ const B = tree.getTaxonByName("B");
118
+ const node = u(tree.getParent(u(tree.getNodeByTaxon(A)))); // current height is 1.
119
+ // const child1 = tree.getChild(node,0)!;
120
+ // const child2 = tree.getChild(node,1)!;
121
+ expect(tree.getHeight(node)).toBe(1);
122
+ const newTree = tree.setHeight(node, 0.5);
123
+ expect(newTree.getLength(node)).toBe(1.5);
124
+ expect(newTree.getHeight(node)).toBe(0.5);
125
+
126
+ expect(newTree.getLength(u(tree.getNodeByTaxon(A)))).toBe(0.5);
127
+ expect(newTree.getLength(u(tree.getNodeByTaxon(B)))).toBe(0.5);
128
+ });
129
+ it("set height test - negative length", function () {
130
+ const tree = ImmutableTree.fromNewick("((A:1,B:1):1,C:2);");
131
+ const A = tree.getTaxonByName("A");
132
+
133
+ const node = tree.getParent(u(tree.getNodeByTaxon(A))); // current height is 1.
134
+ const root = tree.getRoot();
135
+
136
+ const newTree = tree.setHeight(root, 0.5);
137
+ expect(newTree.getLength(node)).toBe(-0.5);
138
+ expect(newTree.getHeight(node)).toBe(1.0);
139
+
140
+ const Cnode = tree.getNode("C");
141
+ expect(newTree.getLength(Cnode)).toBe(0.5);
142
+ // expect(newTree.getLength(tree.getNodeByTaxon(B)!)).toBe(0.5);
143
+ });
144
+ });
145
+ describe("testing annotating", () => {
146
+ it("basic annotation", () => {
147
+ const tree = ImmutableTree.fromNewick("((A:1,B:1):1,C:2);");
148
+ const A = tree.getNode("A");
149
+ const annotatedTree = tree.annotateNode(A, "state", "WA");
150
+ annotatedTree.getAnnotation(A, "state");
151
+ });
152
+ it("basic annotation multiple keys", () => {
153
+ const tree = ImmutableTree.fromNewick("((A:1,B:1):1,C:2);");
154
+ const A = tree.getNode("A");
155
+ const annotatedTree = tree.annotateNode(A, { state: "WA" });
156
+ annotatedTree.getAnnotation(A, "state");
157
+ });
158
+ });