@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,90 @@
|
|
|
1
|
+
import type { layoutOptions } from "../components/figtree/figtree-types";
|
|
2
|
+
//TODO make tree
|
|
3
|
+
|
|
4
|
+
// TODO caching
|
|
5
|
+
|
|
6
|
+
//
|
|
7
|
+
export interface Label {
|
|
8
|
+
x: number;
|
|
9
|
+
y: number;
|
|
10
|
+
alignmentBaseline: string;
|
|
11
|
+
textAnchor: string;
|
|
12
|
+
rotation: number;
|
|
13
|
+
alignedPos?: { x: number; y: number };
|
|
14
|
+
}
|
|
15
|
+
export interface Vertex {
|
|
16
|
+
number: number;
|
|
17
|
+
x: number;
|
|
18
|
+
y: number;
|
|
19
|
+
hidden: boolean | undefined;
|
|
20
|
+
labelHidden: boolean | undefined;
|
|
21
|
+
level: number;
|
|
22
|
+
branch?: {
|
|
23
|
+
d: string;
|
|
24
|
+
label: Label;
|
|
25
|
+
};
|
|
26
|
+
theta?: number; //angle
|
|
27
|
+
r?: number; //radius
|
|
28
|
+
nodeLabel: Label;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ArbitraryVertex {
|
|
32
|
+
hidden: boolean;
|
|
33
|
+
labelHidden: boolean;
|
|
34
|
+
number: number;
|
|
35
|
+
x: number;
|
|
36
|
+
y: number;
|
|
37
|
+
level: number;
|
|
38
|
+
theta?: number; //angle
|
|
39
|
+
pathPoints: { x: number; y: number }[];
|
|
40
|
+
nodeLabel: {
|
|
41
|
+
dx: number;
|
|
42
|
+
dy: number;
|
|
43
|
+
alignmentBaseline: string;
|
|
44
|
+
textAnchor: string;
|
|
45
|
+
rotation?: number;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
//ids match node ids
|
|
50
|
+
export interface Vertices {
|
|
51
|
+
type: "Rectangular" | "Polar" | "Radial";
|
|
52
|
+
vertices: Vertex[];
|
|
53
|
+
origin?: { x: number; y: number }; // used by polar layout to denote the position of the root (or stem) which can change
|
|
54
|
+
theta?: [number, number]; // used by polar layout to denote the range of angles
|
|
55
|
+
axisLength?: number; // provided by layouts that support axis
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface ArbitraryVertices {
|
|
59
|
+
vertices: ArbitraryVertex[];
|
|
60
|
+
extent: { x: [number, number]; y: [number, number] };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface internalLayoutOptions extends layoutOptions {
|
|
64
|
+
// all layout options plus width and height of drawable area
|
|
65
|
+
width?: number;
|
|
66
|
+
height?: number;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface CartoonData {
|
|
70
|
+
cartooned: boolean;
|
|
71
|
+
collapseFactor: number;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const defaultInternalLayoutOptions = {
|
|
75
|
+
width: 1000,
|
|
76
|
+
height: 1000,
|
|
77
|
+
rootLength: 0,
|
|
78
|
+
rootAngle: 0,
|
|
79
|
+
angleRange: 2 * Math.PI - 0.3,
|
|
80
|
+
tipSpace: () => 1,
|
|
81
|
+
curvature: 0,
|
|
82
|
+
showRoot: false,
|
|
83
|
+
spread: 1,
|
|
84
|
+
fishEye: { x: 0, y: 0, scale: 0 },
|
|
85
|
+
cartoonedNodes: new Map(),
|
|
86
|
+
pollard: 0,
|
|
87
|
+
padding: 20,
|
|
88
|
+
invert: false,
|
|
89
|
+
minRadius: 0,
|
|
90
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export interface simpleVertex {
|
|
2
|
+
x: number;
|
|
3
|
+
y: number;
|
|
4
|
+
}
|
|
5
|
+
export interface simplePolarVertex extends simpleVertex {
|
|
6
|
+
theta: number;
|
|
7
|
+
r: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface FunctionalVertex extends simpleVertex {
|
|
11
|
+
layoutClass: layoutClass;
|
|
12
|
+
nodeLabel?: NodeLabelType;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface PolarVertex extends simplePolarVertex {
|
|
16
|
+
layoutClass: layoutClass.Polar;
|
|
17
|
+
nodeLabel?: NodeLabelType;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export enum layoutClass {
|
|
21
|
+
Rectangular = "Rectangular",
|
|
22
|
+
Polar = "Polar",
|
|
23
|
+
Radial = "Radial",
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type NodeLabelType = {
|
|
27
|
+
alignmentBaseline: React.SVGAttributes<SVGTextElement>["alignmentBaseline"];
|
|
28
|
+
textAnchor: React.SVGAttributes<SVGTextElement>["textAnchor"];
|
|
29
|
+
dxFactor: number;
|
|
30
|
+
dyFactor: number;
|
|
31
|
+
rotation: number;
|
|
32
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Normalization of SVG path commands
|
|
2
|
+
// 1. parse path to array of commands - parse-svg-path
|
|
3
|
+
// 2. make all commands absolute - abs-svg-path
|
|
4
|
+
// 3. convert all commands to Curves. normalize-svg-path //cubic bezier curves with 2 control points and the end point
|
|
5
|
+
// 4. split path into n number of curves
|
|
6
|
+
|
|
7
|
+
// curve splitting by https://pomax.github.io/bezierinfo/#introduction
|
|
8
|
+
|
|
9
|
+
import abs from 'abs-svg-path'
|
|
10
|
+
import normalize from 'normalize-svg-path'
|
|
11
|
+
import parse from 'parse-svg-path'
|
|
12
|
+
import type { NormalizedCommand } from './@custom-types/svg-path-types';
|
|
13
|
+
import { unNullify } from './utils';
|
|
14
|
+
|
|
15
|
+
class point{
|
|
16
|
+
x:number;
|
|
17
|
+
y:number;
|
|
18
|
+
constructor(x:number,y:number){
|
|
19
|
+
this.x=x
|
|
20
|
+
this.y=y
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const NUMBER_OF_POINTS=6; // this should be more than needed by layouts
|
|
25
|
+
type curveArray = [string, number, number, number, number, number, number];
|
|
26
|
+
export function normalizePath(path:string):string{ //TODO this might remove the fill on cartoons.
|
|
27
|
+
const parsedPath = parse(path) //as AnyCommand[]
|
|
28
|
+
const absPath = abs(parsedPath)
|
|
29
|
+
const normalizedPath = normalize(absPath) // normalized path is [M, x,y ] [C, x1,y1, x2,y2, x,y]....
|
|
30
|
+
|
|
31
|
+
let newPath = `${normalizedPath[0][0]} ${normalizedPath[0][1]} ${normalizedPath[0][2]} `
|
|
32
|
+
const curves = normalizedPath.filter((d:NormalizedCommand)=>d[0]==="C").map((curve:curveArray)=>{return [new point(curve[1], curve[2]),new point(curve[3], curve[4]),new point(curve[5], curve[6])]})
|
|
33
|
+
|
|
34
|
+
if(curves.length>NUMBER_OF_POINTS){
|
|
35
|
+
throw new Error(`Path must have no more than ${NUMBER_OF_POINTS} nodes (excluding start point) detected ${curves.length} nodes update layout or path.helpers` )
|
|
36
|
+
}
|
|
37
|
+
if(curves.length==0){
|
|
38
|
+
throw new Error('Path must have at least 1 node (excluding start point) update layout or path.helpers' )
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
while(curves.length<NUMBER_OF_POINTS){
|
|
42
|
+
const toSplit = unNullify(curves.pop(),`Internal error in normalization`);
|
|
43
|
+
const {left,right} = splitCubicB(toSplit,0.5);
|
|
44
|
+
curves.push(left);
|
|
45
|
+
curves.push(right.reverse());
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
for(let i = 0; i<curves.length; i++){
|
|
49
|
+
const curve = curves[i];
|
|
50
|
+
newPath+=`C${curve[0].x},${curve[0].y} ${curve[1].x},${curve[1].y} ${curve[2].x},${curve[2].y} `
|
|
51
|
+
}
|
|
52
|
+
return newPath;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function splitCubicB(curve:point[],t:number):{left:point[],right:point[]}{
|
|
56
|
+
const left:point[]=[]
|
|
57
|
+
const right:point[]=[]
|
|
58
|
+
|
|
59
|
+
function getCurve(points:point[],t:number){
|
|
60
|
+
if(points.length==1){
|
|
61
|
+
left.push(points[0])
|
|
62
|
+
right.push(points[0])
|
|
63
|
+
}else{
|
|
64
|
+
const newPoints:point[] = Array(points.length-1) as point[]
|
|
65
|
+
for(let i =0; i<newPoints.length; i++){
|
|
66
|
+
if(i==0){
|
|
67
|
+
left.push(points[0])
|
|
68
|
+
}
|
|
69
|
+
if(i==newPoints.length-1){
|
|
70
|
+
right.push(points[i+1])
|
|
71
|
+
}
|
|
72
|
+
newPoints[i]=new point((1-t)*points[i].x+t*points[i+1].x,(1-t)*points[i].y+t*points[i+1].y)
|
|
73
|
+
}
|
|
74
|
+
getCurve(newPoints,t)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
getCurve(curve,t);
|
|
78
|
+
return {left,right}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { extent } from "d3-array";
|
|
2
|
+
import { scaleLinear } from "d3-scale";
|
|
3
|
+
import type { simplePolarVertex, simpleVertex } from "../layouts/types";
|
|
4
|
+
|
|
5
|
+
export type PolarScaleType = <T extends simpleVertex>(
|
|
6
|
+
vertex: T,
|
|
7
|
+
) => T & { x: number; y: number; r: number; theta: number };
|
|
8
|
+
|
|
9
|
+
export function polarScaleMaker(
|
|
10
|
+
maxX: number,
|
|
11
|
+
maxY: number,
|
|
12
|
+
canvasWidth: number,
|
|
13
|
+
canvasHeight: number,
|
|
14
|
+
invert: boolean = false,
|
|
15
|
+
minRadius: number = 0,
|
|
16
|
+
angleRange: number = 1.7 * Math.PI,
|
|
17
|
+
rootAngle: number = 0,
|
|
18
|
+
pollard = 0,
|
|
19
|
+
) {
|
|
20
|
+
const maxRadius = Math.min(canvasWidth, canvasHeight) / 2;
|
|
21
|
+
|
|
22
|
+
// These scales adjust the x and y values from arbitrary layout to polar coordinates with r within the svg and theta between 0 and 2pi
|
|
23
|
+
|
|
24
|
+
const safeAngleRange = normalizeAngle(angleRange);
|
|
25
|
+
const minX = maxX * pollard;
|
|
26
|
+
const rRange = invert
|
|
27
|
+
? [minRadius * maxRadius, maxRadius].reverse()
|
|
28
|
+
: [minRadius * maxRadius, maxRadius];
|
|
29
|
+
const rScale = scaleLinear().domain([minX, maxX]).range(rRange);
|
|
30
|
+
|
|
31
|
+
const startAngle = rootAngle + (2 * 3.14 - safeAngleRange) / 2; //2pi - angle range is what we ignore and we want to center this on the root angle
|
|
32
|
+
const endAngle = startAngle + safeAngleRange;
|
|
33
|
+
|
|
34
|
+
const thetaScale = scaleLinear()
|
|
35
|
+
.domain([0, maxY])
|
|
36
|
+
.range([startAngle, endAngle]); // rotated to match figtree orientation
|
|
37
|
+
|
|
38
|
+
// (x,y) =>polarToCartesian(rScale(x),thetaScale(y))=>(x,y)
|
|
39
|
+
|
|
40
|
+
// Once we have the polar coordinates we will convert back to cartesian coordinates
|
|
41
|
+
// But we need to adjust the aspect ratio to fit the circle
|
|
42
|
+
|
|
43
|
+
// center (0,0) polartoCartesian(maxRadius,startAngle) is top left of svg
|
|
44
|
+
const extremes = [
|
|
45
|
+
[0, 0],
|
|
46
|
+
polarToCartesian(maxRadius, startAngle),
|
|
47
|
+
polarToCartesian(maxRadius, endAngle),
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
// Also need every pi/2 point we pass through.
|
|
51
|
+
//assumes range is <=2pi
|
|
52
|
+
const normlizedStart = normalizeAngle(startAngle);
|
|
53
|
+
const normlizedEnd = normalizeAngle(normlizedStart + safeAngleRange);
|
|
54
|
+
|
|
55
|
+
if (normlizedEnd > normlizedStart) {
|
|
56
|
+
for (const theta of [Math.PI / 2, Math.PI, (3 * Math.PI) / 2].filter(
|
|
57
|
+
(d) => d > normlizedStart && d < normlizedEnd,
|
|
58
|
+
)) {
|
|
59
|
+
const [x, y] = polarToCartesian(maxRadius, theta);
|
|
60
|
+
extremes.push([x, y]);
|
|
61
|
+
}
|
|
62
|
+
} else {
|
|
63
|
+
//we've crossed 0
|
|
64
|
+
|
|
65
|
+
for (const theta of [0, Math.PI / 2, Math.PI, (3 * Math.PI) / 2].filter(
|
|
66
|
+
(d) => d > normlizedStart || d < normlizedEnd,
|
|
67
|
+
)) {
|
|
68
|
+
const [x, y] = polarToCartesian(maxRadius, theta);
|
|
69
|
+
extremes.push([x, y]);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const xDomain = extent(extremes, (d) => d[0]) as [number, number];
|
|
74
|
+
const yDomain = extent(extremes, (d) => d[1]) as [number, number];
|
|
75
|
+
|
|
76
|
+
const ratio = (xDomain[1] - xDomain[0]) / (yDomain[1] - yDomain[0]);
|
|
77
|
+
|
|
78
|
+
const scaler = Math.min(canvasWidth, canvasHeight * ratio);
|
|
79
|
+
const width = scaler;
|
|
80
|
+
const height = scaler / ratio;
|
|
81
|
+
|
|
82
|
+
const xShift = (canvasWidth - width) / 2;
|
|
83
|
+
const yShift = (canvasHeight - height) / 2;
|
|
84
|
+
|
|
85
|
+
const yRange = [yShift, canvasHeight - yShift];
|
|
86
|
+
const xRange = [xShift, canvasWidth - xShift];
|
|
87
|
+
|
|
88
|
+
const x = scaleLinear().domain(xDomain).range(xRange);
|
|
89
|
+
const y = scaleLinear().domain(yDomain).range(yRange);
|
|
90
|
+
|
|
91
|
+
return function scale<T extends simpleVertex>(
|
|
92
|
+
vertex: T,
|
|
93
|
+
): T & simplePolarVertex {
|
|
94
|
+
// const [r,theta] =[rScale(vertex.x),normalizeAngle(thetaScale(vertex.y))];
|
|
95
|
+
const [r, theta] = [rScale(vertex.x), thetaScale(vertex.y)]; // not normalized so we get branch length arc directions correct.
|
|
96
|
+
const [xcart, ycart] = polarToCartesian(r, theta);
|
|
97
|
+
|
|
98
|
+
const nTheta = normalizeAngle(theta); // normalized so we can think straight when doing things with text.
|
|
99
|
+
|
|
100
|
+
const nodeLabel = {
|
|
101
|
+
alignmentBaseline: "middle",
|
|
102
|
+
textAnchor:
|
|
103
|
+
nTheta > Math.PI / 2 && nTheta < (3 * Math.PI) / 2 ? "end" : " start",
|
|
104
|
+
dxFactor: Math.cos(nTheta),
|
|
105
|
+
dyFactor: Math.sin(nTheta),
|
|
106
|
+
rotation: textSafeDegrees(nTheta),
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
...vertex,
|
|
111
|
+
x: x(xcart),
|
|
112
|
+
y: y(ycart),
|
|
113
|
+
r,
|
|
114
|
+
theta,
|
|
115
|
+
nodeLabel: nodeLabel,
|
|
116
|
+
};
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function polarToCartesian(r: number, theta: number) {
|
|
121
|
+
return [r * Math.cos(theta), r * Math.sin(theta)];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function normalizeAngle(theta: number) {
|
|
125
|
+
while (theta > 2 * Math.PI) {
|
|
126
|
+
theta -= 2 * Math.PI;
|
|
127
|
+
}
|
|
128
|
+
return theta;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function degrees(theta: number) {
|
|
132
|
+
return (normalizeAngle(theta) * 180) / Math.PI;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
//this function converts radians to degrees and adjusts degrees
|
|
136
|
+
// so the text is not fliped
|
|
137
|
+
export function textSafeDegrees(radians: number) {
|
|
138
|
+
const d = degrees(normalizeAngle(radians));
|
|
139
|
+
|
|
140
|
+
if (d > 90 && d < 270) {
|
|
141
|
+
return d - 180;
|
|
142
|
+
} else {
|
|
143
|
+
return d;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import type { NodeRef } from "../evo/tree";
|
|
2
|
+
import type { FunctionalVertex, NodeLabelType, simpleVertex } from "../layouts";
|
|
3
|
+
import { layoutClass } from "../layouts";
|
|
4
|
+
import { polarScaleMaker } from "./polar-scale";
|
|
5
|
+
import type { ScaleLinear } from "d3-scale";
|
|
6
|
+
import { scaleLinear } from "d3-scale";
|
|
7
|
+
|
|
8
|
+
//todo remove props that are just the getting passed to children and use render props instead
|
|
9
|
+
|
|
10
|
+
export type layoutType = (n: NodeRef) => FunctionalVertex;
|
|
11
|
+
|
|
12
|
+
//Todo cache these
|
|
13
|
+
export type scaleOptions = {
|
|
14
|
+
domainX: number[];
|
|
15
|
+
domainY: number[];
|
|
16
|
+
canvasWidth: number;
|
|
17
|
+
canvasHeight: number;
|
|
18
|
+
layoutClass: layoutClass;
|
|
19
|
+
invert?: boolean;
|
|
20
|
+
minRadius?: number;
|
|
21
|
+
angleRange?: number;
|
|
22
|
+
rootAngle?: number;
|
|
23
|
+
rootLength?: number;
|
|
24
|
+
pollard: number;
|
|
25
|
+
fishEye?: { x: number; y: number; scale: number };
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const defaultNodeLabelData: NodeLabelType = {
|
|
29
|
+
alignmentBaseline: "middle",
|
|
30
|
+
textAnchor: "middle",
|
|
31
|
+
dxFactor: 1,
|
|
32
|
+
dyFactor: 1,
|
|
33
|
+
rotation: 0,
|
|
34
|
+
};
|
|
35
|
+
// scale adds x and y to whatever comes in.
|
|
36
|
+
export type scaleType = <T extends simpleVertex>(
|
|
37
|
+
vertex: T,
|
|
38
|
+
) => T & { x: number; y: number };
|
|
39
|
+
// export type scale = scaleType<simpleVertex> // TODO use generics to clean up!
|
|
40
|
+
|
|
41
|
+
export function getScale({
|
|
42
|
+
domainX,
|
|
43
|
+
domainY,
|
|
44
|
+
canvasWidth,
|
|
45
|
+
canvasHeight,
|
|
46
|
+
layoutClass: layoutType,
|
|
47
|
+
invert = false,
|
|
48
|
+
minRadius = 0,
|
|
49
|
+
angleRange = 2 * Math.PI,
|
|
50
|
+
rootAngle = 0,
|
|
51
|
+
pollard = 0,
|
|
52
|
+
fishEye = { x: 0, y: 0, scale: 0 },
|
|
53
|
+
}: scaleOptions): scaleType {
|
|
54
|
+
let xScale: ScaleLinear<number, number>;
|
|
55
|
+
let yScale: ScaleLinear<number, number>;
|
|
56
|
+
|
|
57
|
+
switch (layoutType) {
|
|
58
|
+
case layoutClass.Rectangular: {
|
|
59
|
+
const minX = domainX[1] * pollard;
|
|
60
|
+
xScale = scaleLinear().domain([minX, domainX[1]]).range([0, canvasWidth]); // 0 to account for any root length
|
|
61
|
+
yScale = scaleLinear().domain(domainY).range([0, canvasHeight]);
|
|
62
|
+
|
|
63
|
+
let calcY = (n: number) => yScale(n);
|
|
64
|
+
if (fishEye.scale > 0) {
|
|
65
|
+
const y = yScale.invert(fishEye.y);
|
|
66
|
+
const transform = fishEyeTransform(fishEye.scale, y);
|
|
67
|
+
const newYScale = yScale.copy().domain(yScale.domain().map(transform));
|
|
68
|
+
calcY = (y: number) => newYScale(transform(y));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (invert) {
|
|
72
|
+
xScale.range([canvasWidth, 0]);
|
|
73
|
+
}
|
|
74
|
+
return function scale<T extends simpleVertex>(
|
|
75
|
+
vertex: T,
|
|
76
|
+
): T & { x: number; y: number } {
|
|
77
|
+
return { ...vertex, x: xScale(vertex.x), y: calcY(vertex.y) };
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
case layoutClass.Polar:
|
|
81
|
+
return polarScaleMaker(
|
|
82
|
+
domainX[1],
|
|
83
|
+
domainY[1],
|
|
84
|
+
canvasWidth,
|
|
85
|
+
canvasHeight,
|
|
86
|
+
invert,
|
|
87
|
+
minRadius,
|
|
88
|
+
angleRange,
|
|
89
|
+
rootAngle,
|
|
90
|
+
pollard,
|
|
91
|
+
);
|
|
92
|
+
case layoutClass.Radial: {
|
|
93
|
+
//TODO need to update so x and y scales are equal otherwise horizontal branches will have a different scale than vertical branches
|
|
94
|
+
// scale x and y to 0 1
|
|
95
|
+
// then scale to standard witdth height.
|
|
96
|
+
const normalizeX = scaleLinear().domain(domainX).range([0, 1]);
|
|
97
|
+
const normalizeY = scaleLinear().domain(domainY).range([0, 1]);
|
|
98
|
+
const maxRange = Math.min(canvasWidth, canvasHeight);
|
|
99
|
+
// const scaler = Math.min(canvasWidth,canvasHeight*ratio)
|
|
100
|
+
// const width = scaler;
|
|
101
|
+
// const height = scaler/ratio;
|
|
102
|
+
// center in window
|
|
103
|
+
const xShift = (canvasWidth - maxRange) / 2;
|
|
104
|
+
const yShift = (canvasHeight - maxRange) / 2;
|
|
105
|
+
|
|
106
|
+
const xRange = [xShift, maxRange + xShift];
|
|
107
|
+
const yRange = [yShift, maxRange + yShift];
|
|
108
|
+
|
|
109
|
+
yScale = scaleLinear().domain([0, 1]).range(yRange);
|
|
110
|
+
xScale = scaleLinear().domain([0, 1]).range(xRange);
|
|
111
|
+
return function scale<T extends simpleVertex>(
|
|
112
|
+
vertex: T,
|
|
113
|
+
): T & { x: number; y: number } {
|
|
114
|
+
return {
|
|
115
|
+
...vertex,
|
|
116
|
+
x: xScale(normalizeX(vertex.x)),
|
|
117
|
+
y: yScale(normalizeY(vertex.y)),
|
|
118
|
+
};
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
default:
|
|
122
|
+
throw new Error("Not implemented in calcX");
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Figtree cc Andrew Rambaut
|
|
127
|
+
export const fishEyeTransform =
|
|
128
|
+
(fishEye: number, pointOfInterestY: number) => (y: number) => {
|
|
129
|
+
// point of interest is in layout scale.
|
|
130
|
+
|
|
131
|
+
if (fishEye === 0.0) {
|
|
132
|
+
return y;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const scale = 1.0 / fishEye;
|
|
136
|
+
const dist = pointOfInterestY - y;
|
|
137
|
+
const min = 1.0 - pointOfInterestY / (scale + pointOfInterestY);
|
|
138
|
+
const max =
|
|
139
|
+
1.0 - (pointOfInterestY - 1.0) / (scale - (pointOfInterestY - 1.0));
|
|
140
|
+
|
|
141
|
+
const c = 1.0 - (dist < 0 ? dist / (scale - dist) : dist / (scale + dist));
|
|
142
|
+
|
|
143
|
+
return (c - min) / (max - min);
|
|
144
|
+
};
|