@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.
- package/CHANGELOG.md +9 -0
- package/eslint.config.js +9 -0
- package/package.json +76 -0
- package/src/@custom-types/normalize-svg-path.d.ts +13 -0
- package/src/@custom-types/parse-svg-path.d.ts +8 -0
- package/src/@custom-types/svg-path-types.d.ts +37 -0
- package/src/bauble-makers/makers.ts +112 -0
- package/src/bauble-makers/set-up-baubles.ts +197 -0
- package/src/bauble-makers/utils.ts +61 -0
- package/src/components/baubles/bauble.tsx +61 -0
- package/src/components/baubles/branches.tsx +13 -0
- package/src/components/baubles/clades/cartoon.tsx +68 -0
- package/src/components/baubles/clades/highlight.tsx +96 -0
- package/src/components/baubles/clades/index.ts +1 -0
- package/src/components/baubles/clades.tsx +45 -0
- package/src/components/baubles/helpers.tsx +62 -0
- package/src/components/baubles/index.ts +16 -0
- package/src/components/baubles/labels.tsx +38 -0
- package/src/components/baubles/nodes.tsx +51 -0
- package/src/components/baubles/shapes/branch.tsx +53 -0
- package/src/components/baubles/shapes/circle.tsx +64 -0
- package/src/components/baubles/shapes/index.ts +9 -0
- package/src/components/baubles/shapes/label.tsx +104 -0
- package/src/components/baubles/shapes/rectangle.tsx +83 -0
- package/src/components/baubles/types.ts +99 -0
- package/src/components/decorations/axis/axis-types.ts +123 -0
- package/src/components/decorations/axis/axis.tsx +21 -0
- package/src/components/decorations/axis/index.ts +2 -0
- package/src/components/decorations/axis/polar-axis-bars.tsx +102 -0
- package/src/components/decorations/axis/polar-axis.tsx +175 -0
- package/src/components/decorations/axis/rectangular-axis-bars.tsx +53 -0
- package/src/components/decorations/axis/rectangular-axis.tsx +151 -0
- package/src/components/decorations/index.ts +2 -0
- package/src/components/decorations/legend/discrete-legend.tsx +93 -0
- package/src/components/decorations/legend/index.ts +1 -0
- package/src/components/decorations/legend/legend.tsx +1 -0
- package/src/components/figtree/figtree-types.ts +69 -0
- package/src/components/figtree/figtree.tsx +136 -0
- package/src/components/figtree/index.ts +3 -0
- package/src/components/hoc/index.ts +7 -0
- package/src/components/hoc/with-branch.tsx +148 -0
- package/src/components/hoc/with-branches.tsx +54 -0
- package/src/components/hoc/with-clades.tsx +47 -0
- package/src/components/hoc/with-node.tsx +183 -0
- package/src/components/hoc/with-nodes.tsx +45 -0
- package/src/components/index.ts +4 -0
- package/src/context/aminated-context.ts +3 -0
- package/src/context/dimension-context.ts +22 -0
- package/src/context/layout-context.ts +20 -0
- package/src/context/scale-context.ts +12 -0
- package/src/evo/index.ts +1 -0
- package/src/evo/tree/index.ts +5 -0
- package/src/evo/tree/mcc-tree.ts +0 -0
- package/src/evo/tree/normalized-tree/immutable-tree-helpers.ts +136 -0
- package/src/evo/tree/normalized-tree/immutable-tree.test.ts +158 -0
- package/src/evo/tree/normalized-tree/immutable-tree.ts +1365 -0
- package/src/evo/tree/normalized-tree/index.ts +3 -0
- package/src/evo/tree/parsers/annotation-parser.ts +276 -0
- package/src/evo/tree/parsers/index.ts +3 -0
- package/src/evo/tree/parsers/newick-character-parser.ts +246 -0
- package/src/evo/tree/parsers/newick-parsing.ts +22 -0
- package/src/evo/tree/parsers/nexus-parser.ts +12 -0
- package/src/evo/tree/parsers/nexus-parsing.ts +68 -0
- package/src/evo/tree/parsers/parsing.test.ts +289 -0
- package/src/evo/tree/parsers/stream-reader/index.ts +1 -0
- package/src/evo/tree/parsers/stream-reader/newick-importer.txt +395 -0
- package/src/evo/tree/parsers/stream-reader/nexus-importer.test.ts +99 -0
- package/src/evo/tree/parsers/stream-reader/nexus-importer.ts +293 -0
- package/src/evo/tree/parsers/stream-reader/nexus-tokenizer.ts +77 -0
- package/src/evo/tree/parsers/stream-reader/nexus-transform-stream.txt +109 -0
- package/src/evo/tree/taxa/helper-functions.ts +46 -0
- package/src/evo/tree/taxa/index.ts +1 -0
- package/src/evo/tree/taxa/taxon.ts +116 -0
- package/src/evo/tree/traversals/index.ts +1 -0
- package/src/evo/tree/traversals/preorder-traversal.ts +89 -0
- package/src/evo/tree/traversals/traversal-types.ts +6 -0
- package/src/evo/tree/tree-types.ts +197 -0
- package/src/evo/tree/utilities.ts +44 -0
- package/src/index.ts +6 -0
- package/src/layouts/functional/index.ts +2 -0
- package/src/layouts/functional/radial-layout.ts +150 -0
- package/src/layouts/functional/rectangular-layout.ts +71 -0
- package/src/layouts/index.ts +3 -0
- package/src/layouts/layout-interface.ts +90 -0
- package/src/layouts/types.ts +32 -0
- package/src/path.helpers.ts +81 -0
- package/src/store/polar-scale.ts +145 -0
- package/src/store/store.ts +144 -0
- package/src/tests/baubles/__snapshots__/branch-labels.test.tsx.snap +901 -0
- package/src/tests/baubles/__snapshots__/node-labels.test.tsx.snap +1516 -0
- package/src/tests/baubles/branch-labels.test.tsx +103 -0
- package/src/tests/baubles/label.svg +131 -0
- package/src/tests/baubles/node-labels.test.tsx +126 -0
- package/src/tests/clades/__snapshots__/cartoon.test.tsx.snap +327 -0
- package/src/tests/clades/__snapshots__/highlight.test.tsx.snap +337 -0
- package/src/tests/clades/cartoon.test.tsx +65 -0
- package/src/tests/clades/highlight.test.tsx +66 -0
- package/src/tests/figtree/__snapshots__/figtree.test.tsx.snap +761 -0
- package/src/tests/figtree/figtree.test.tsx +123 -0
- package/src/tests/figtree/simple.svg +47 -0
- package/src/tests/layouts/radiallayout.test.ts +23 -0
- package/src/tests/layouts/rectangularlayout.test.ts +65 -0
- package/src/tests/shapes/branch.test.tsx +40 -0
- package/src/tests/shapes/circle.test.tsx +47 -0
- package/src/tests/shapes/label.test.tsx +101 -0
- package/src/tests/shapes/rectangle.test.tsx +67 -0
- package/src/tests/shapes/types.ts +1 -0
- package/src/utils.ts +57 -0
- package/tsconfig.json +12 -0
- package/vite.config.ts +34 -0
- package/vitetest.config.ts +11 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { MaybeType } from "@figtreejs/maybe/maybe";
|
|
2
|
+
import { maybeGetNameFromIndex, maybeGetTaxonByName } from "./helper-functions";
|
|
3
|
+
|
|
4
|
+
export interface Taxon {
|
|
5
|
+
name: string;
|
|
6
|
+
number: number;
|
|
7
|
+
annotations: { [annotation: string]: string | string[] | number | number[] };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function newTaxon(name: string, number: number): Taxon {
|
|
11
|
+
return {
|
|
12
|
+
name,
|
|
13
|
+
number,
|
|
14
|
+
annotations: {},
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface TaxonSetInterface {
|
|
19
|
+
addTaxon(name: string): TaxonSetInterface;
|
|
20
|
+
getTaxon(id: number): Taxon | undefined;
|
|
21
|
+
getTaxonByName(name: string): Taxon;
|
|
22
|
+
getTaxonCount(): number;
|
|
23
|
+
lockTaxa(): TaxonSetInterface;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface TaxonSetData {
|
|
27
|
+
allNames: string[];
|
|
28
|
+
byName: { [taxon: string]: Taxon };
|
|
29
|
+
finalized: boolean;
|
|
30
|
+
}
|
|
31
|
+
export class TaxonSet implements TaxonSetInterface {
|
|
32
|
+
_data: TaxonSetData;
|
|
33
|
+
constructor(taxonSetData?: TaxonSetData) {
|
|
34
|
+
this._data = taxonSetData
|
|
35
|
+
? taxonSetData
|
|
36
|
+
: {
|
|
37
|
+
allNames: [],
|
|
38
|
+
byName: {},
|
|
39
|
+
finalized: false,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
lockTaxa(): TaxonSetInterface {
|
|
43
|
+
if (!this._data.finalized) {
|
|
44
|
+
this._data.finalized = true;
|
|
45
|
+
}
|
|
46
|
+
return this;
|
|
47
|
+
}
|
|
48
|
+
addTaxon(taxonOrName: string | Taxon): this {
|
|
49
|
+
if (this._data.finalized) {
|
|
50
|
+
throw new Error("Cannot add taxon to finalized set");
|
|
51
|
+
}
|
|
52
|
+
let taxon: Taxon;
|
|
53
|
+
if (typeof taxonOrName === "string") {
|
|
54
|
+
const name = taxonOrName;
|
|
55
|
+
|
|
56
|
+
if (Object.prototype.hasOwnProperty.call(this._data.byName, name)) {
|
|
57
|
+
throw new Error(
|
|
58
|
+
`taxon ${name} already exists in the set. Names must be unique`,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
taxon = newTaxon(name, this._data.allNames.length);
|
|
63
|
+
} else {
|
|
64
|
+
taxon = taxonOrName;
|
|
65
|
+
if (Object.prototype.hasOwnProperty.call(this._data.byName, taxon.name)) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
`taxon ${taxon.name} already exists in the set. Names must be unique`,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
if (
|
|
71
|
+
this._data.allNames[taxon.number] &&
|
|
72
|
+
this._data.allNames[taxon.number] !== taxon.name
|
|
73
|
+
) {
|
|
74
|
+
throw new Error(
|
|
75
|
+
`taxon number ${taxon.number} already exists in the set with name ${this._data.allNames[taxon.number]}. Taxon numbers must be unique`,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
console.log("Adding existing taxon:", taxon.name);
|
|
79
|
+
}
|
|
80
|
+
this._data.allNames[taxon.number] = taxon.name;
|
|
81
|
+
this._data.byName[taxon.name] = taxon;
|
|
82
|
+
return this;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
getTaxon(id: number): Taxon {
|
|
86
|
+
const taxon = maybeGetTaxonByName(
|
|
87
|
+
this._data,
|
|
88
|
+
maybeGetNameFromIndex(this._data, id),
|
|
89
|
+
);
|
|
90
|
+
switch (taxon.type) {
|
|
91
|
+
case MaybeType.Some:
|
|
92
|
+
return taxon.value;
|
|
93
|
+
case MaybeType.Nothing:
|
|
94
|
+
throw new Error(`Taxon by name ${id} not found`); // won't get here I dont' think
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
getTaxonByName(name: string): Taxon {
|
|
98
|
+
const taxon = maybeGetTaxonByName(this._data, name);
|
|
99
|
+
switch (taxon.type) {
|
|
100
|
+
case MaybeType.Some:
|
|
101
|
+
return taxon.value;
|
|
102
|
+
case MaybeType.Nothing:
|
|
103
|
+
throw new Error(`Taxon by name ${name} not found`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
hasTaxon(id: string): boolean {
|
|
107
|
+
return Object.prototype.hasOwnProperty.call(this._data.byName, id);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
getTaxonCount(): number {
|
|
111
|
+
return this._data.allNames.length;
|
|
112
|
+
}
|
|
113
|
+
get isFinalized() {
|
|
114
|
+
return this._data.finalized;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { PreOrderTraversalCache } from "./preorder-traversal";
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { Tree, NodeRef } from "../tree-types";
|
|
2
|
+
import type { TreeTraversal } from "./traversal-types";
|
|
3
|
+
export class PreOrderTraversalCache implements TreeTraversal {
|
|
4
|
+
_forwardCache: Map<NodeRef, NodeRef>;
|
|
5
|
+
_reverseCache: Map<NodeRef, NodeRef>;
|
|
6
|
+
constructor() {
|
|
7
|
+
this._forwardCache = new Map();
|
|
8
|
+
this._reverseCache = new Map();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
*traverse(tree: Tree, node?: NodeRef): Generator<NodeRef> {
|
|
12
|
+
const traverse = function* (node: NodeRef): Generator<NodeRef> {
|
|
13
|
+
const childCount = tree.getChildCount(node);
|
|
14
|
+
|
|
15
|
+
if (childCount > 0) {
|
|
16
|
+
for (let i = 0; i < childCount; i++) {
|
|
17
|
+
const child = tree.getChild(node, i);
|
|
18
|
+
yield* traverse(child);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
yield node;
|
|
22
|
+
};
|
|
23
|
+
if (node === undefined) {
|
|
24
|
+
node = tree.getRoot();
|
|
25
|
+
}
|
|
26
|
+
yield* traverse(node);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
//Check we've been to next otherwise we need to update again.
|
|
30
|
+
getNext(tree: Tree, node: NodeRef): NodeRef | undefined {
|
|
31
|
+
const n = this._forwardCache.get(node);
|
|
32
|
+
if (n !== undefined) {
|
|
33
|
+
if (this._forwardCache.get(n) !== undefined) {
|
|
34
|
+
return n; //if this node hasn't changed nor the next
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (tree.isRoot(node)) {
|
|
39
|
+
//start over
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
const parent = tree.getParent(node);
|
|
43
|
+
if (tree.hasRightSibling(node)) {
|
|
44
|
+
const rs = tree.getRightSibling(node);
|
|
45
|
+
this._forwardCache.set(node, rs);
|
|
46
|
+
this._reverseCache.set(rs, node);
|
|
47
|
+
} else {
|
|
48
|
+
this._forwardCache.set(node, parent);
|
|
49
|
+
this._reverseCache.set(parent, node);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return this._forwardCache.get(node);
|
|
53
|
+
}
|
|
54
|
+
getPrevious(tree: Tree, node: NodeRef): NodeRef | undefined {
|
|
55
|
+
const n = this._reverseCache.get(node);
|
|
56
|
+
if (n !== undefined) {
|
|
57
|
+
if (this._reverseCache.get(n) !== undefined) {
|
|
58
|
+
return n; //if this node hasn't changed nor the next
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (node === this.traverse(tree).next().value) {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
if (tree.isInternal(node)) {
|
|
65
|
+
const childCount = tree.getChildCount(node) - 1;
|
|
66
|
+
const lastChild = tree.getChild(node, childCount);
|
|
67
|
+
this._reverseCache.set(node, lastChild);
|
|
68
|
+
this._forwardCache.set(lastChild, node);
|
|
69
|
+
} else {
|
|
70
|
+
if (tree.hasLeftSibling(node)) {
|
|
71
|
+
const ls = tree.getLeftSibling(node);
|
|
72
|
+
this._reverseCache.set(node, ls);
|
|
73
|
+
this._forwardCache.set(ls, node);
|
|
74
|
+
} else {
|
|
75
|
+
let n = node;
|
|
76
|
+
while (!tree.hasLeftSibling(n)) {
|
|
77
|
+
// aunt = tree.getLeftSibling(n);
|
|
78
|
+
n = tree.getParent(n);
|
|
79
|
+
}
|
|
80
|
+
const aunt = tree.getLeftSibling(n);
|
|
81
|
+
this._reverseCache.set(node, aunt);
|
|
82
|
+
this._forwardCache.set(aunt, node);
|
|
83
|
+
// look for parent's left sibling if none try again until root
|
|
84
|
+
// if at and no ls error we are at first tip and should have caught this above.
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return this._reverseCache.get(node);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import type { Taxon, TaxonSet } from "./taxa/taxon";
|
|
2
|
+
|
|
3
|
+
export interface NodeRef {
|
|
4
|
+
number: number;
|
|
5
|
+
_id: string;
|
|
6
|
+
}
|
|
7
|
+
export enum BaseAnnotationType {
|
|
8
|
+
DISCRETE = "DISCRETE", // string could also be stringy numbers
|
|
9
|
+
BOOLEAN = "BOOLEAN", // true false
|
|
10
|
+
NUMERICAL = "NUMERICAL", // float
|
|
11
|
+
|
|
12
|
+
NUMERICAL_SET = "NUMERICAL_SET", // number[]
|
|
13
|
+
DISCRETE_SET = "DISCRETE_SET", // string[]
|
|
14
|
+
|
|
15
|
+
MARKOV_JUMPS = "MARKOV_JUMPS", // {from: to: time:}
|
|
16
|
+
DENSITIES = "DENSITIES", // Record<string, number>
|
|
17
|
+
}
|
|
18
|
+
export type MarkovJumpValue = { from: string; to: string; time?: number };
|
|
19
|
+
|
|
20
|
+
export type ValueOf<T extends BaseAnnotationType> =
|
|
21
|
+
T extends BaseAnnotationType.DISCRETE
|
|
22
|
+
? string
|
|
23
|
+
: T extends BaseAnnotationType.BOOLEAN
|
|
24
|
+
? boolean
|
|
25
|
+
: T extends BaseAnnotationType.NUMERICAL
|
|
26
|
+
? number
|
|
27
|
+
: T extends BaseAnnotationType.NUMERICAL_SET
|
|
28
|
+
? number[]
|
|
29
|
+
: T extends BaseAnnotationType.DISCRETE_SET
|
|
30
|
+
? string[]
|
|
31
|
+
: T extends BaseAnnotationType.MARKOV_JUMPS
|
|
32
|
+
? MarkovJumpValue[]
|
|
33
|
+
: T extends BaseAnnotationType.DENSITIES
|
|
34
|
+
? Record<string, number>
|
|
35
|
+
: never;
|
|
36
|
+
|
|
37
|
+
export type RawValueOf<T extends BaseAnnotationType> =
|
|
38
|
+
T extends BaseAnnotationType.DISCRETE
|
|
39
|
+
? string
|
|
40
|
+
: T extends BaseAnnotationType.BOOLEAN
|
|
41
|
+
? boolean
|
|
42
|
+
: T extends BaseAnnotationType.NUMERICAL
|
|
43
|
+
? number
|
|
44
|
+
: T extends BaseAnnotationType.NUMERICAL_SET
|
|
45
|
+
? number[]
|
|
46
|
+
: T extends BaseAnnotationType.DISCRETE_SET
|
|
47
|
+
? string[]
|
|
48
|
+
: T extends BaseAnnotationType.MARKOV_JUMPS
|
|
49
|
+
? [number, string, string][] | [string, string, string][]
|
|
50
|
+
: T extends BaseAnnotationType.DENSITIES
|
|
51
|
+
? Record<string, number>
|
|
52
|
+
: never;
|
|
53
|
+
|
|
54
|
+
export type DomainOf<T extends BaseAnnotationType> =
|
|
55
|
+
T extends BaseAnnotationType.DISCRETE
|
|
56
|
+
? string[]
|
|
57
|
+
: T extends BaseAnnotationType.BOOLEAN
|
|
58
|
+
? [boolean, boolean]
|
|
59
|
+
: T extends BaseAnnotationType.NUMERICAL
|
|
60
|
+
? [number, number]
|
|
61
|
+
: T extends BaseAnnotationType.NUMERICAL_SET
|
|
62
|
+
? [number, number]
|
|
63
|
+
: T extends BaseAnnotationType.DISCRETE_SET
|
|
64
|
+
? string[] | number[]
|
|
65
|
+
: T extends BaseAnnotationType.MARKOV_JUMPS
|
|
66
|
+
? string[] // just locations
|
|
67
|
+
: T extends BaseAnnotationType.DENSITIES
|
|
68
|
+
? string[] // just states
|
|
69
|
+
: never;
|
|
70
|
+
|
|
71
|
+
/** A single, generic annotation, discriminated by `type`. */
|
|
72
|
+
export type AbstractAnnotation<T extends BaseAnnotationType> = {
|
|
73
|
+
id: string;
|
|
74
|
+
type: T;
|
|
75
|
+
value: ValueOf<T>;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export type Annotation = {
|
|
79
|
+
[K in BaseAnnotationType]: AbstractAnnotation<K>;
|
|
80
|
+
}[BaseAnnotationType];
|
|
81
|
+
|
|
82
|
+
export interface AbstractAnnotationSummary<T extends BaseAnnotationType> {
|
|
83
|
+
id: string;
|
|
84
|
+
type: T;
|
|
85
|
+
domain: DomainOf<T>;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export type AnnotationDomain = {
|
|
89
|
+
[K in BaseAnnotationType]: DomainOf<K>;
|
|
90
|
+
}[BaseAnnotationType];
|
|
91
|
+
export type AnnotationValue = {
|
|
92
|
+
[K in BaseAnnotationType]: ValueOf<K>;
|
|
93
|
+
}[BaseAnnotationType];
|
|
94
|
+
export type AnnotationSummary = {
|
|
95
|
+
[K in BaseAnnotationType]: AbstractAnnotationSummary<K>;
|
|
96
|
+
}[BaseAnnotationType];
|
|
97
|
+
export type RawAnnotationValue = {
|
|
98
|
+
[K in BaseAnnotationType]: RawValueOf<K>;
|
|
99
|
+
}[BaseAnnotationType];
|
|
100
|
+
|
|
101
|
+
export interface newickParsingOptions {
|
|
102
|
+
dateFormat?: string;
|
|
103
|
+
datePrefix?: string;
|
|
104
|
+
labelName?: string;
|
|
105
|
+
parseAnnotations?: boolean;
|
|
106
|
+
tipNameMap?: Map<string, string>;
|
|
107
|
+
taxonSet?: TaxonSet;
|
|
108
|
+
}
|
|
109
|
+
export interface Tree {
|
|
110
|
+
getRoot(): NodeRef;
|
|
111
|
+
isRooted(): boolean;
|
|
112
|
+
getNodeCount(): number;
|
|
113
|
+
getInternalNodeCount(): number;
|
|
114
|
+
getExternalNodeCount(): number;
|
|
115
|
+
getNode(i: string | Taxon | number): NodeRef;
|
|
116
|
+
getInternalNodes(): NodeRef[];
|
|
117
|
+
getExternalNodes(): NodeRef[];
|
|
118
|
+
getNodes(): NodeRef[];
|
|
119
|
+
getTaxon(id: number | NodeRef): Taxon;
|
|
120
|
+
|
|
121
|
+
isExternal(node: NodeRef): boolean;
|
|
122
|
+
isInternal(node: NodeRef): boolean;
|
|
123
|
+
isRoot(node: NodeRef): boolean;
|
|
124
|
+
|
|
125
|
+
getChildCount(node: NodeRef): number;
|
|
126
|
+
getChild(node: NodeRef, i: number): NodeRef;
|
|
127
|
+
|
|
128
|
+
getNodeByTaxon(taxon: Taxon): NodeRef;
|
|
129
|
+
getNodeByLabel(label: string): NodeRef;
|
|
130
|
+
// getLevel(node:NodeRef):number;
|
|
131
|
+
|
|
132
|
+
getDivergence(node: NodeRef): number;
|
|
133
|
+
getHeight(node: NodeRef): number;
|
|
134
|
+
getLength(node: NodeRef): number;
|
|
135
|
+
|
|
136
|
+
getParent(node: NodeRef): NodeRef;
|
|
137
|
+
getChildren(node: NodeRef): NodeRef[];
|
|
138
|
+
|
|
139
|
+
getAnnotation(
|
|
140
|
+
node: NodeRef,
|
|
141
|
+
name: string,
|
|
142
|
+
d?: AnnotationValue,
|
|
143
|
+
): AnnotationValue; // what about or undefined?
|
|
144
|
+
// getAnnotation<T extends BaseAnnotationType>(node: NodeRef, name: string, d:T): T
|
|
145
|
+
|
|
146
|
+
// getAnnotationValue<T extends BaseAnnotationType>(node:NodeRef, name:string,type:T):ValueOf<T>|undefined
|
|
147
|
+
// annotateNode<T extends BaseAnnotationType>(node:NodeRef,annotation:{name:string,value:RawValueOf<T>}):Tree
|
|
148
|
+
annotateNode(node: NodeRef, name: string, value: RawAnnotationValue): Tree;
|
|
149
|
+
annotateNode(
|
|
150
|
+
node: NodeRef,
|
|
151
|
+
annotation: Record<string, RawAnnotationValue>,
|
|
152
|
+
): Tree;
|
|
153
|
+
|
|
154
|
+
getLabel(node: NodeRef): string;
|
|
155
|
+
hasLabel(node: NodeRef): boolean;
|
|
156
|
+
|
|
157
|
+
getAnnotationKeys(): string[];
|
|
158
|
+
getAnnotationType(name: string): BaseAnnotationType;
|
|
159
|
+
|
|
160
|
+
getAnnotations(): AnnotationSummary[];
|
|
161
|
+
getAnnotationSummary(name: string): AnnotationSummary;
|
|
162
|
+
|
|
163
|
+
addNodes(n?: number): { tree: Tree; nodes: NodeRef[] };
|
|
164
|
+
deleteNode(n: NodeRef): Tree;
|
|
165
|
+
removeChild(parent: NodeRef, child: NodeRef): Tree;
|
|
166
|
+
deleteClade(n: NodeRef): Tree;
|
|
167
|
+
// hasNextSibling(node:Node)
|
|
168
|
+
getNextSibling(node: NodeRef): NodeRef;
|
|
169
|
+
hasRightSibling(node: NodeRef): boolean;
|
|
170
|
+
getRightSibling(node: NodeRef): NodeRef;
|
|
171
|
+
hasLeftSibling(node: NodeRef): boolean;
|
|
172
|
+
getLeftSibling(node: NodeRef): NodeRef;
|
|
173
|
+
|
|
174
|
+
setHeight(node: NodeRef, height: number): Tree;
|
|
175
|
+
setDivergence(node: NodeRef, divergence: number): Tree;
|
|
176
|
+
setLength(node: NodeRef, length: number): Tree;
|
|
177
|
+
setTaxon(node: NodeRef, taxon: Taxon): Tree;
|
|
178
|
+
setLabel(node: NodeRef, label: string): Tree;
|
|
179
|
+
|
|
180
|
+
addChild(parent: NodeRef, child: NodeRef): Tree;
|
|
181
|
+
// root(node: NodeRef,portion:number): Tree
|
|
182
|
+
unroot(node: NodeRef): Tree;
|
|
183
|
+
toNewick(node?: NodeRef, options?: { includeAnnotations: boolean }): string;
|
|
184
|
+
orderNodesByDensity(down: boolean): Tree;
|
|
185
|
+
sortChildren(
|
|
186
|
+
node: NodeRef,
|
|
187
|
+
compare: (a: NodeRef, b: NodeRef) => number,
|
|
188
|
+
): Tree;
|
|
189
|
+
isRoot(node: NodeRef): boolean;
|
|
190
|
+
getMRCA(node1: NodeRef, node2: NodeRef): NodeRef;
|
|
191
|
+
getMRCA(nodes: NodeRef[]): NodeRef;
|
|
192
|
+
rotate(node: NodeRef, recursive: boolean): Tree;
|
|
193
|
+
reroot(node: NodeRef, proportion: number): Tree;
|
|
194
|
+
}
|
|
195
|
+
export type TreeListener = (tree: Tree, node: NodeRef) => void;
|
|
196
|
+
|
|
197
|
+
//TODO abstact Tree with parsing implementations
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
//https://stackoverflow.com/questions/27936772/how-to-deep-merge-instead-of-shallow-merge
|
|
2
|
+
import { timeParse, timeFormat } from "d3-time-format";
|
|
3
|
+
|
|
4
|
+
//https://stackoverflow.com/questions/29400171/how-do-i-convert-a-decimal-year-value-into-a-date-in-javascript
|
|
5
|
+
/**
|
|
6
|
+
* Helper function to determine if the provided year is a leap year
|
|
7
|
+
* @param year
|
|
8
|
+
* @return {boolean}
|
|
9
|
+
*/
|
|
10
|
+
export function leapYear(year: number) {
|
|
11
|
+
return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A function which converts a decimal float into a date object
|
|
16
|
+
* @param decimalDate
|
|
17
|
+
* @return {Date}
|
|
18
|
+
*/
|
|
19
|
+
export function decimalToDate(decimal: number) {
|
|
20
|
+
const year = Math.trunc(decimal);
|
|
21
|
+
const totalNumberOfDays = leapYear(year) ? 366 : 365;
|
|
22
|
+
const day = Math.round((decimal - year) * totalNumberOfDays);
|
|
23
|
+
return timeParse("%Y-%j")(`${year}-${day}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A function that converts a date into a decimal.
|
|
28
|
+
* @param date
|
|
29
|
+
* @return {number}
|
|
30
|
+
*/
|
|
31
|
+
export function dateToDecimal(date: Date) {
|
|
32
|
+
const year = parseInt(timeFormat("%Y")(date));
|
|
33
|
+
const day = parseInt(timeFormat("%j")(date));
|
|
34
|
+
const totalNumberOfDays = leapYear(year) ? 366 : 365;
|
|
35
|
+
return year + day / totalNumberOfDays;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
//https://stackoverflow.com/questions/526559/testing-if-something-is-a-class-in-javascript
|
|
39
|
+
export function isFunction(funcOrClass: unknown) {
|
|
40
|
+
const propertyNames = Object.getOwnPropertyNames(funcOrClass);
|
|
41
|
+
return (
|
|
42
|
+
!propertyNames.includes("prototype") || propertyNames.includes("arguments")
|
|
43
|
+
);
|
|
44
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// export function layout (tree:ImmutableTree,node?:NodeRef):Map<NodeRef,FunctionalVertex>{
|
|
2
|
+
|
|
3
|
+
import type { ImmutableTree, NodeRef } from "../../evo";
|
|
4
|
+
import { preOrderIterator, tipIterator } from "../../evo";
|
|
5
|
+
import type { NodeLabelType } from "../types";
|
|
6
|
+
import { notNull } from "../../utils";
|
|
7
|
+
import type { FunctionalVertex } from "../types";
|
|
8
|
+
import { layoutClass } from "../types";
|
|
9
|
+
|
|
10
|
+
// //todo set some map for fixing the traversal of the tree.
|
|
11
|
+
// const vertexMap = new Map();
|
|
12
|
+
type data = {
|
|
13
|
+
angleStart: number;
|
|
14
|
+
angleEnd: number;
|
|
15
|
+
xpos: number;
|
|
16
|
+
ypos: number;
|
|
17
|
+
level: number;
|
|
18
|
+
number: number;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export function radialLayout(
|
|
22
|
+
tree: ImmutableTree,
|
|
23
|
+
options: { spread?: number } = {},
|
|
24
|
+
): (node: NodeRef) => FunctionalVertex {
|
|
25
|
+
const { spread = 1 } = options;
|
|
26
|
+
console.log("radial layout with spread", spread);
|
|
27
|
+
const map = new Map<NodeRef, FunctionalVertex>();
|
|
28
|
+
|
|
29
|
+
const dataStack: data[] = [
|
|
30
|
+
{
|
|
31
|
+
angleStart: 0,
|
|
32
|
+
angleEnd: 2 * Math.PI,
|
|
33
|
+
xpos: 0,
|
|
34
|
+
ypos: 0,
|
|
35
|
+
level: 0,
|
|
36
|
+
number: tree.getRoot().number,
|
|
37
|
+
},
|
|
38
|
+
]; // TODO start tree.
|
|
39
|
+
for (const node of preOrderIterator(tree)) {
|
|
40
|
+
const data = dataStack.pop();
|
|
41
|
+
notNull(data, `Internal Error, hit the end of the data stack unexpectedly`);
|
|
42
|
+
const { angleStart, angleEnd, xpos, ypos, level } = data;
|
|
43
|
+
|
|
44
|
+
const branchAngle = (angleStart + angleEnd) / 2.0;
|
|
45
|
+
|
|
46
|
+
const length = !tree.isRoot(node) ? tree.getLength(node) : 0;
|
|
47
|
+
|
|
48
|
+
const directionX = Math.cos(branchAngle);
|
|
49
|
+
const directionY = Math.sin(branchAngle);
|
|
50
|
+
const x = xpos + length * directionX;
|
|
51
|
+
const y = ypos + length * directionY;
|
|
52
|
+
|
|
53
|
+
const leftLabel = tree.getChildCount(node) > 0;
|
|
54
|
+
let dx, dy;
|
|
55
|
+
if (!leftLabel) {
|
|
56
|
+
dx = Math.cos(branchAngle);
|
|
57
|
+
dy = Math.sin(branchAngle);
|
|
58
|
+
} else {
|
|
59
|
+
dx = Math.cos(branchAngle);
|
|
60
|
+
dy = Math.sin(branchAngle);
|
|
61
|
+
}
|
|
62
|
+
const vertex = {
|
|
63
|
+
x,
|
|
64
|
+
y,
|
|
65
|
+
layoutClass: layoutClass.Radial,
|
|
66
|
+
theta: branchAngle,
|
|
67
|
+
nodeLabel: {
|
|
68
|
+
dxFactor: dx,
|
|
69
|
+
dyFactor: dy,
|
|
70
|
+
alignmentBaseline: "middle",
|
|
71
|
+
textAnchor:
|
|
72
|
+
normalizeAngle(branchAngle) > Math.PI / 2 &&
|
|
73
|
+
normalizeAngle(branchAngle) < (3 * Math.PI) / 2
|
|
74
|
+
? "end"
|
|
75
|
+
: "start",
|
|
76
|
+
rotation: 0, // textSafeDegrees(normalizeAngle(branchAngle))
|
|
77
|
+
} as NodeLabelType,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
if (tree.getChildCount(node) > 0) {
|
|
81
|
+
const childLeafs: number[] = [];
|
|
82
|
+
let totalLeafs = 0;
|
|
83
|
+
for (let i = 0; i < tree.getChildCount(node); i++) {
|
|
84
|
+
const leafCount = [...tipIterator(tree, tree.getChild(node, i))].length;
|
|
85
|
+
childLeafs[i] = leafCount;
|
|
86
|
+
totalLeafs += leafCount;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let span = angleEnd - angleStart;
|
|
90
|
+
let updatedAngleStart = angleStart;
|
|
91
|
+
|
|
92
|
+
if (tree.getRoot() !== node) {
|
|
93
|
+
// span *= 1.0 + ((safeOpts.spread * Math.PI / 180) / 10.0);
|
|
94
|
+
span *= 1.0 + (spread * Math.PI) / 180 / 10.0;
|
|
95
|
+
updatedAngleStart = branchAngle - span / 2.0;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
let a2 = updatedAngleStart;
|
|
99
|
+
|
|
100
|
+
for (let i = tree.getChildCount(node) - 1; i > -1; i--) {
|
|
101
|
+
// i think we need to go in reverse order here
|
|
102
|
+
const a1 = a2;
|
|
103
|
+
a2 = a1 + (span * childLeafs[i]) / totalLeafs;
|
|
104
|
+
dataStack.push({
|
|
105
|
+
angleStart: a1,
|
|
106
|
+
angleEnd: a2,
|
|
107
|
+
xpos: x,
|
|
108
|
+
ypos: y,
|
|
109
|
+
level: level + 1,
|
|
110
|
+
number: tree.getChild(node, i).number,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
map.set(node, vertex);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return function (node: NodeRef): FunctionalVertex {
|
|
118
|
+
if (map.has(node)) {
|
|
119
|
+
return map.get(node) as FunctionalVertex; // must be here since we check it above
|
|
120
|
+
} else {
|
|
121
|
+
throw new Error("Node not found in layout - has the tree changed");
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
//this function converts radians to degrees and adjusts degrees
|
|
127
|
+
// so the text is not fliped
|
|
128
|
+
export function textSafeDegrees(radians: number) {
|
|
129
|
+
const d = degrees(radians);
|
|
130
|
+
//trial and error - must be a better way
|
|
131
|
+
if (d > 90 && d < 270) {
|
|
132
|
+
return (d - 180) / 2;
|
|
133
|
+
} else if (d > 0 && d < 88) {
|
|
134
|
+
return d / 2;
|
|
135
|
+
} else if (d < 360 && d > 272) {
|
|
136
|
+
return (360 + d) / 2;
|
|
137
|
+
} else {
|
|
138
|
+
return d;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
export function normalizeAngle(theta: number) {
|
|
142
|
+
while (theta > 2 * Math.PI) {
|
|
143
|
+
theta -= 2 * Math.PI;
|
|
144
|
+
}
|
|
145
|
+
return theta;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function degrees(theta: number) {
|
|
149
|
+
return (normalizeAngle(theta) * 180) / Math.PI;
|
|
150
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { mean } from "d3-array";
|
|
2
|
+
import type { ImmutableTree, NodeRef } from "../../evo";
|
|
3
|
+
import { postOrderIterator } from "../../evo";
|
|
4
|
+
import type { NodeLabelType, FunctionalVertex } from "../types";
|
|
5
|
+
import { layoutClass } from "../types";
|
|
6
|
+
import { unNullify } from "../../utils";
|
|
7
|
+
|
|
8
|
+
export function baseLayout(lc: layoutClass) {
|
|
9
|
+
function layout(tree: ImmutableTree): (node: NodeRef) => FunctionalVertex {
|
|
10
|
+
const map = new Map<NodeRef, FunctionalVertex>();
|
|
11
|
+
|
|
12
|
+
let currentY = 0;
|
|
13
|
+
for (const node of postOrderIterator(tree)) {
|
|
14
|
+
let protoVertex: { x: number; y: number };
|
|
15
|
+
const x = tree.getDivergence(node);
|
|
16
|
+
const leftLabel = tree.getChildCount(node) > 0;
|
|
17
|
+
const hasParent = !tree.isRoot(node);
|
|
18
|
+
const labelBelow =
|
|
19
|
+
tree.getChildCount(node) > 0 &&
|
|
20
|
+
(!hasParent || tree.getChild(tree.getParent(node), 0) !== node);
|
|
21
|
+
|
|
22
|
+
if (tree.isExternal(node)) {
|
|
23
|
+
protoVertex = { x, y: currentY };
|
|
24
|
+
currentY++;
|
|
25
|
+
} else {
|
|
26
|
+
const kidPositions = tree
|
|
27
|
+
.getChildren(node)
|
|
28
|
+
.map((child) =>
|
|
29
|
+
unNullify(
|
|
30
|
+
map.get(child),
|
|
31
|
+
`Internal Error: child not yet found in layout`,
|
|
32
|
+
),
|
|
33
|
+
);
|
|
34
|
+
const y = unNullify(
|
|
35
|
+
mean(kidPositions, (d) => d.y),
|
|
36
|
+
`Error taking the mean of child positions`,
|
|
37
|
+
);
|
|
38
|
+
protoVertex = { x, y };
|
|
39
|
+
}
|
|
40
|
+
const vertex = {
|
|
41
|
+
...protoVertex,
|
|
42
|
+
layoutClass: lc,
|
|
43
|
+
nodeLabel: {
|
|
44
|
+
alignmentBaseline: leftLabel
|
|
45
|
+
? labelBelow
|
|
46
|
+
? "bottom"
|
|
47
|
+
: "hanging"
|
|
48
|
+
: "middle", // todo calc on the fly
|
|
49
|
+
textAnchor: leftLabel ? "end" : "start",
|
|
50
|
+
dxFactor: leftLabel ? -1 : 1,
|
|
51
|
+
dyFactor: leftLabel ? (labelBelow ? -1 : 1) : 0,
|
|
52
|
+
rotation: 0,
|
|
53
|
+
} as NodeLabelType,
|
|
54
|
+
};
|
|
55
|
+
map.set(node, vertex);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return function (node: NodeRef): FunctionalVertex {
|
|
59
|
+
if (map.has(node)) {
|
|
60
|
+
return map.get(node) as FunctionalVertex; // check above so
|
|
61
|
+
} else {
|
|
62
|
+
console.log(node);
|
|
63
|
+
throw new Error("Node not found in layout - has the tree changed");
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
return layout;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export const rectangularLayout = baseLayout(layoutClass.Rectangular);
|
|
71
|
+
export const polarLayout = baseLayout(layoutClass.Polar);
|