@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,151 @@
|
|
|
1
|
+
import { useContext } from "react";
|
|
2
|
+
import { line } from "d3-shape";
|
|
3
|
+
import { mean } from "d3-array";
|
|
4
|
+
import type { ScaleContinuousNumeric } from "d3-scale";
|
|
5
|
+
import type {
|
|
6
|
+
AxisOrientation,
|
|
7
|
+
AxisProps,
|
|
8
|
+
WorkingTipOptions,
|
|
9
|
+
} from "./axis-types";
|
|
10
|
+
import { defaultAxisProps } from "./axis-types";
|
|
11
|
+
import { makeAxisScale } from "./polar-axis";
|
|
12
|
+
import { unNullify } from "../../../utils";
|
|
13
|
+
import { DimensionContext } from "../../../context/dimension-context";
|
|
14
|
+
import { ScaleContext } from "../../../context/scale-context";
|
|
15
|
+
import RectangleAxisBars from "./rectangular-axis-bars";
|
|
16
|
+
|
|
17
|
+
export default function Axis(props: AxisProps) {
|
|
18
|
+
// add bar options to props
|
|
19
|
+
const dimensions = useContext(DimensionContext);
|
|
20
|
+
// const {layoutClass} = dimensions
|
|
21
|
+
|
|
22
|
+
const figureScale = useContext(ScaleContext);
|
|
23
|
+
|
|
24
|
+
const {
|
|
25
|
+
// direction = defaultAxisProps.direction,
|
|
26
|
+
gap = defaultAxisProps.gap,
|
|
27
|
+
bars,
|
|
28
|
+
} = props;
|
|
29
|
+
|
|
30
|
+
const attrs = { ...defaultAxisProps.attrs, ...props.attrs };
|
|
31
|
+
const ticks: WorkingTipOptions = props.ticks
|
|
32
|
+
? { ...defaultAxisProps.ticks, ...props.ticks }
|
|
33
|
+
: defaultAxisProps.ticks;
|
|
34
|
+
const title = props.title
|
|
35
|
+
? { ...defaultAxisProps.title, ...props.title }
|
|
36
|
+
: defaultAxisProps.title;
|
|
37
|
+
|
|
38
|
+
// todo options to provide tick values so can specify breaks
|
|
39
|
+
|
|
40
|
+
const scale = makeAxisScale(props, dimensions);
|
|
41
|
+
|
|
42
|
+
// scaleSequentialQuantile doesn’t implement tickValues or tickFormat.
|
|
43
|
+
let tickValues: number[];
|
|
44
|
+
if (ticks.values !== undefined) {
|
|
45
|
+
tickValues = ticks.values;
|
|
46
|
+
} else {
|
|
47
|
+
// if (!scale.ticks) {
|
|
48
|
+
// const tickNum = unNullify(ticks.number,`No values provided for ticks, and scale does not provide ticks function. Please specify the target number of ticks`)
|
|
49
|
+
// tickValues = range(tickNum).map((i) =>
|
|
50
|
+
// quantile(scale.domain(), i / (tickNum - 1)),
|
|
51
|
+
// ) as number[]
|
|
52
|
+
// } else {
|
|
53
|
+
tickValues = scale.ticks(ticks.number);
|
|
54
|
+
// }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const axisY = dimensions.domainY[1] + dimensions.domainY[1] * 0.01;
|
|
58
|
+
const start = figureScale({ x: dimensions.domainX[0], y: axisY });
|
|
59
|
+
const end = figureScale({ x: dimensions.domainX[1], y: axisY });
|
|
60
|
+
const axisPath = `M${start.x},${start.y + gap} L${end.x},${end.y + gap}`;
|
|
61
|
+
|
|
62
|
+
//TODO break this into parts HOC with logic horizontal/ vertical axis ect.
|
|
63
|
+
const xPos = unNullify(
|
|
64
|
+
mean(scale.range()),
|
|
65
|
+
`Error calculating x position for title`,
|
|
66
|
+
);
|
|
67
|
+
const titlePos = figureScale({ x: xPos, y: axisY });
|
|
68
|
+
return (
|
|
69
|
+
<g className={"axis"}>
|
|
70
|
+
{/*This is for Bars*/}
|
|
71
|
+
|
|
72
|
+
{bars ? (
|
|
73
|
+
<RectangleAxisBars
|
|
74
|
+
{...bars}
|
|
75
|
+
tickValues={tickValues}
|
|
76
|
+
scale={scale}
|
|
77
|
+
axisY={axisY}
|
|
78
|
+
/>
|
|
79
|
+
) : null}
|
|
80
|
+
|
|
81
|
+
<path d={axisPath} stroke={"black"} {...attrs} />
|
|
82
|
+
<g>
|
|
83
|
+
{tickValues.map((t, i) => {
|
|
84
|
+
const point = figureScale({ x: scale(t), y: axisY });
|
|
85
|
+
return (
|
|
86
|
+
<g
|
|
87
|
+
key={`tick-${i}`}
|
|
88
|
+
transform={`translate(${point.x},${point.y + gap})`}
|
|
89
|
+
>
|
|
90
|
+
<line
|
|
91
|
+
x1={0}
|
|
92
|
+
y1={0}
|
|
93
|
+
x2={0}
|
|
94
|
+
y2={ticks.length}
|
|
95
|
+
stroke={"black"}
|
|
96
|
+
{...attrs}
|
|
97
|
+
/>
|
|
98
|
+
<text
|
|
99
|
+
transform={`translate(0,${ticks.padding})`}
|
|
100
|
+
textAnchor={"middle"}
|
|
101
|
+
dominantBaseline={"central"}
|
|
102
|
+
{...ticks.style}
|
|
103
|
+
>
|
|
104
|
+
{ticks.format(t)}
|
|
105
|
+
</text>
|
|
106
|
+
</g>
|
|
107
|
+
);
|
|
108
|
+
})}
|
|
109
|
+
{/*TODO sometimes scale doesn't have a range*/}
|
|
110
|
+
<g transform={`translate(${titlePos.x},${titlePos.y + gap}) `}>
|
|
111
|
+
<text
|
|
112
|
+
textAnchor={"middle"}
|
|
113
|
+
transform={`translate(0,${title.padding})`}
|
|
114
|
+
{...title.style}
|
|
115
|
+
>
|
|
116
|
+
{title.text}
|
|
117
|
+
</text>
|
|
118
|
+
</g>
|
|
119
|
+
</g>
|
|
120
|
+
</g>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
//TODO merge these in instead of overwriting;
|
|
124
|
+
|
|
125
|
+
export function getPath(
|
|
126
|
+
scale: ScaleContinuousNumeric<number, number>,
|
|
127
|
+
direction: AxisOrientation,
|
|
128
|
+
): string {
|
|
129
|
+
const f = line()
|
|
130
|
+
.x((d) => d[0])
|
|
131
|
+
.y((d) => d[1]);
|
|
132
|
+
|
|
133
|
+
let range;
|
|
134
|
+
switch (direction) {
|
|
135
|
+
case "vertical":
|
|
136
|
+
range = scale.range().map<[number, number]>((d) => [0, d]);
|
|
137
|
+
break;
|
|
138
|
+
default:
|
|
139
|
+
range = scale.range().map<[number, number]>((d) => [d, 0]);
|
|
140
|
+
}
|
|
141
|
+
const rangeLine = unNullify(f(range), `Error in makeing axis line`);
|
|
142
|
+
|
|
143
|
+
return rangeLine;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function getTickLine(length: number, direction: AxisOrientation) {
|
|
147
|
+
if (direction === "horizontal" || direction === "polar") {
|
|
148
|
+
return { x1: 0, y1: 0, y2: length, x2: 0 };
|
|
149
|
+
}
|
|
150
|
+
return { x1: 0, y1: 0, y2: 0, x2: -1 * length };
|
|
151
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { ScaleOrdinal } from "d3-scale";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ContinuousLegend
|
|
5
|
+
*
|
|
6
|
+
* A color legend that accept continuous and sequential color scales. It is modeled after the color legends
|
|
7
|
+
* at https://observablehq.com/@d3/color-legend
|
|
8
|
+
*
|
|
9
|
+
* @param props
|
|
10
|
+
* @param scale
|
|
11
|
+
* @param pos
|
|
12
|
+
* @param width
|
|
13
|
+
* @param height
|
|
14
|
+
* @param title
|
|
15
|
+
* @return {(number|*)[]|*}
|
|
16
|
+
* @constructor
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
interface DiscreteLegendProps {
|
|
20
|
+
scale: ScaleOrdinal<string, string>;
|
|
21
|
+
pos: { x: number; y: number };
|
|
22
|
+
width: number;
|
|
23
|
+
height: number;
|
|
24
|
+
swatchSize: number;
|
|
25
|
+
format: (s: string) => string;
|
|
26
|
+
fontSize: number;
|
|
27
|
+
columns: number;
|
|
28
|
+
title: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function DiscreteLegend(props: DiscreteLegendProps) {
|
|
32
|
+
// const dispatch = useInteractionsDispatch();
|
|
33
|
+
// const onHover=useCallback((value)=>()=>dispatch({type:"hover",payload:{dataType:DataType.DISCRETE,key:annotation,value:value}}))
|
|
34
|
+
// const onUnHover = useCallback(()=>dispatch({type:"unhover",payload:{}}));
|
|
35
|
+
// TODO safari doesn't like the legend with two columns maybe move out of svg but need to scale and position so behaves the same
|
|
36
|
+
const { scale, pos, width, height, swatchSize, format, columns, fontSize } =
|
|
37
|
+
props;
|
|
38
|
+
|
|
39
|
+
const numEntries = scale.domain().length;
|
|
40
|
+
const maxEntriesPerColumn = Math.ceil(numEntries / columns);
|
|
41
|
+
const columnStarts = [];
|
|
42
|
+
const xGap = width / columns;
|
|
43
|
+
const yGap = height / maxEntriesPerColumn;
|
|
44
|
+
for (let i = 0; i < columns; i++) {
|
|
45
|
+
columnStarts.push(i * xGap);
|
|
46
|
+
}
|
|
47
|
+
const rowStarts = [];
|
|
48
|
+
for (let i = 0; i < maxEntriesPerColumn; i++) {
|
|
49
|
+
rowStarts.push(i * yGap);
|
|
50
|
+
}
|
|
51
|
+
const swatchPositions = [];
|
|
52
|
+
for (let i = 0; i < numEntries; i++) {
|
|
53
|
+
swatchPositions.push({
|
|
54
|
+
x: columnStarts[i % columns],
|
|
55
|
+
y: rowStarts[i % maxEntriesPerColumn],
|
|
56
|
+
text: scale.domain()[i],
|
|
57
|
+
color: scale(scale.domain()[i]),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// add a font size option with title being bigger than swatch text
|
|
62
|
+
// add swatch size option too.
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<g transform={`translate(${pos.x},${pos.y})`}>
|
|
66
|
+
<text
|
|
67
|
+
textAnchor="start"
|
|
68
|
+
x={0}
|
|
69
|
+
y={0}
|
|
70
|
+
className="legend-title"
|
|
71
|
+
fontSize={`${fontSize + 4}px`}
|
|
72
|
+
>
|
|
73
|
+
{props.title}
|
|
74
|
+
</text>
|
|
75
|
+
<g transform={`translate(0,20)`}>
|
|
76
|
+
{swatchPositions.map((d, i) => (
|
|
77
|
+
<g key={i} transform={`translate(${d.x},${d.y})`}>
|
|
78
|
+
<rect width={swatchSize} height={swatchSize} fill={d.color} />
|
|
79
|
+
<text
|
|
80
|
+
textAnchor="start"
|
|
81
|
+
dominantBaseline={"middle"}
|
|
82
|
+
x={swatchSize + 5}
|
|
83
|
+
dy={swatchSize / 2}
|
|
84
|
+
fontSize={`${fontSize}px`}
|
|
85
|
+
>
|
|
86
|
+
{format(d.text)}
|
|
87
|
+
</text>
|
|
88
|
+
</g>
|
|
89
|
+
))}
|
|
90
|
+
</g>
|
|
91
|
+
</g>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { DiscreteLegend } from "./discrete-legend";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
//This will decide what kind of legend to render based on the provided color scale.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { InternalBaubleOptions } from "../../bauble-makers/set-up-baubles";
|
|
2
|
+
import type { ImmutableTree, NodeRef, Tree } from "../../evo/tree";
|
|
3
|
+
import type { FunctionalVertex, layoutClass } from "../../layouts/types";
|
|
4
|
+
import type { layoutType, scaleType } from "../../store/store";
|
|
5
|
+
import type { AxisProps } from "../decorations";
|
|
6
|
+
|
|
7
|
+
export interface Margins {
|
|
8
|
+
top: number;
|
|
9
|
+
bottom: number;
|
|
10
|
+
left: number;
|
|
11
|
+
right: number;
|
|
12
|
+
}
|
|
13
|
+
export type fishEyeOptions = { x: number; y: number; scale: number };
|
|
14
|
+
|
|
15
|
+
export interface dimensionType extends layoutOptions {
|
|
16
|
+
canvasWidth: number;
|
|
17
|
+
canvasHeight: number;
|
|
18
|
+
domainX: [number, number];
|
|
19
|
+
domainY: [number, number];
|
|
20
|
+
layoutClass: layoutClass;
|
|
21
|
+
invert: boolean;
|
|
22
|
+
pollard: number;
|
|
23
|
+
minRadius: number;
|
|
24
|
+
fishEye: fishEyeOptions;
|
|
25
|
+
rootAngle: number;
|
|
26
|
+
angleRange: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface layoutOptions {
|
|
30
|
+
rootLength?: number;
|
|
31
|
+
rootAngle?: number;
|
|
32
|
+
angleRange?: number;
|
|
33
|
+
showRoot?: boolean;
|
|
34
|
+
spread?: number;
|
|
35
|
+
fishEye?: fishEyeOptions;
|
|
36
|
+
pollard: number;
|
|
37
|
+
invert?: boolean;
|
|
38
|
+
minRadius?: number;
|
|
39
|
+
padding?: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
type layoutFunction = (
|
|
43
|
+
tree: ImmutableTree,
|
|
44
|
+
options?: layoutOptions,
|
|
45
|
+
) => (node: NodeRef) => FunctionalVertex;
|
|
46
|
+
|
|
47
|
+
//TODO sort this out with dimensions
|
|
48
|
+
export type BaubleTypes = {
|
|
49
|
+
tree: Tree;
|
|
50
|
+
scale: scaleType;
|
|
51
|
+
layout: layoutType;
|
|
52
|
+
dimensions: dimensionType;
|
|
53
|
+
animated?: boolean;
|
|
54
|
+
};
|
|
55
|
+
// type Bauble= React.FC<BaubleTypes>
|
|
56
|
+
|
|
57
|
+
export interface FigtreeProps {
|
|
58
|
+
width: number;
|
|
59
|
+
height: number;
|
|
60
|
+
layout: layoutFunction;
|
|
61
|
+
tree: ImmutableTree;
|
|
62
|
+
margins?: Margins;
|
|
63
|
+
baubles?: InternalBaubleOptions[];
|
|
64
|
+
opts?: layoutOptions;
|
|
65
|
+
animated?: boolean;
|
|
66
|
+
x?: number;
|
|
67
|
+
y?: number;
|
|
68
|
+
axis?: AxisProps;
|
|
69
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import type { FigtreeProps } from "./figtree-types";
|
|
2
|
+
import { defaultInternalLayoutOptions, rectangularLayout } from "../../layouts";
|
|
3
|
+
|
|
4
|
+
import { ImmutableTree } from "../../evo/tree";
|
|
5
|
+
import { getScale } from "../../store/store";
|
|
6
|
+
import { extent } from "d3-array";
|
|
7
|
+
import { unNullify } from "../../utils";
|
|
8
|
+
import { Bauble } from "../baubles/bauble";
|
|
9
|
+
|
|
10
|
+
import { ScaleContext } from "../../context/scale-context";
|
|
11
|
+
import { layoutContext } from "../../context/layout-context";
|
|
12
|
+
import { animatedContext } from "../../context/aminated-context";
|
|
13
|
+
import { setupBaubles } from "../../bauble-makers/set-up-baubles";
|
|
14
|
+
import { Branches } from "../../bauble-makers/makers";
|
|
15
|
+
import { DimensionContext } from "../../context/dimension-context";
|
|
16
|
+
import { Axis } from "../decorations";
|
|
17
|
+
/**
|
|
18
|
+
* The FigTree component
|
|
19
|
+
* This takes a tree and layout options. It calls the layout and handles state for this figure.
|
|
20
|
+
* It also passes it's scales to it's children props as well as the edges to the branches and the nodes to the nodes.
|
|
21
|
+
*/
|
|
22
|
+
//TODO extract these from state to props?
|
|
23
|
+
//TODO this is different than defualt
|
|
24
|
+
const defaultTree = ImmutableTree.fromNewick("((A:1,B:1):1,C:2);"); //TODO don't expose the need to pass a tree in here.
|
|
25
|
+
|
|
26
|
+
export const defaultOpts = {
|
|
27
|
+
opts: defaultInternalLayoutOptions,
|
|
28
|
+
width: 100,
|
|
29
|
+
height: 100,
|
|
30
|
+
layout: rectangularLayout,
|
|
31
|
+
margins: { top: 10, right: 10, bottom: 10, left: 10 },
|
|
32
|
+
tree: defaultTree,
|
|
33
|
+
baubles: [
|
|
34
|
+
Branches({
|
|
35
|
+
filter: () => true,
|
|
36
|
+
attrs: { stroke: "black", strokeWidth: 1 },
|
|
37
|
+
}),
|
|
38
|
+
],
|
|
39
|
+
animated: false,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
//TODO animated not provided to user for each bauble
|
|
43
|
+
|
|
44
|
+
function FigTree(props: FigtreeProps) {
|
|
45
|
+
const {
|
|
46
|
+
width = defaultOpts.width,
|
|
47
|
+
height = defaultOpts.width,
|
|
48
|
+
margins = defaultOpts.margins,
|
|
49
|
+
tree = defaultOpts.tree,
|
|
50
|
+
layout = defaultOpts.layout,
|
|
51
|
+
animated = defaultOpts.animated,
|
|
52
|
+
baubles = defaultOpts.baubles,
|
|
53
|
+
// margins = defaultOpts.margins
|
|
54
|
+
axis,
|
|
55
|
+
} = props;
|
|
56
|
+
|
|
57
|
+
const opts = props.opts ? props.opts : defaultOpts.opts;
|
|
58
|
+
|
|
59
|
+
const {
|
|
60
|
+
rootAngle = defaultOpts.opts.rootAngle,
|
|
61
|
+
angleRange = defaultOpts.opts.angleRange,
|
|
62
|
+
fishEye = defaultOpts.opts.fishEye,
|
|
63
|
+
pollard = defaultOpts.opts.pollard,
|
|
64
|
+
minRadius = defaultOpts.opts.minRadius,
|
|
65
|
+
invert = defaultOpts.opts.invert,
|
|
66
|
+
} = opts;
|
|
67
|
+
//todo this requires opts to not be undefined even though all the values are optional.
|
|
68
|
+
let canvasWidth;
|
|
69
|
+
let canvasHeight;
|
|
70
|
+
let { x, y } = props;
|
|
71
|
+
|
|
72
|
+
if (x !== undefined && y !== undefined) {
|
|
73
|
+
// if x and y are provide then these give the top left corner and width and height represent the whole area.
|
|
74
|
+
canvasWidth = width;
|
|
75
|
+
canvasHeight = height;
|
|
76
|
+
} else {
|
|
77
|
+
canvasWidth = width - margins.left - margins.right;
|
|
78
|
+
canvasHeight = height - margins.top - margins.bottom;
|
|
79
|
+
x = margins.left;
|
|
80
|
+
y = margins.top;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const layoutMap = layout(tree, opts);
|
|
84
|
+
const { layoutClass } = layoutMap(tree.getRoot());
|
|
85
|
+
const domainX = extent(tree.getNodes().map((n) => layoutMap(n).x)).map((d) =>
|
|
86
|
+
unNullify(d, `Error finding x extent from layout`),
|
|
87
|
+
) as [number, number];
|
|
88
|
+
const domainY = extent(tree.getNodes().map((n) => layoutMap(n).y)).map((d) =>
|
|
89
|
+
unNullify(d, `Error finding y extent from layout`),
|
|
90
|
+
) as [number, number];
|
|
91
|
+
|
|
92
|
+
const dimensions = {
|
|
93
|
+
canvasWidth,
|
|
94
|
+
canvasHeight,
|
|
95
|
+
domainX,
|
|
96
|
+
domainY,
|
|
97
|
+
layoutClass,
|
|
98
|
+
invert,
|
|
99
|
+
pollard,
|
|
100
|
+
minRadius,
|
|
101
|
+
fishEye,
|
|
102
|
+
rootAngle,
|
|
103
|
+
angleRange,
|
|
104
|
+
};
|
|
105
|
+
const scale = getScale(dimensions);
|
|
106
|
+
|
|
107
|
+
const baubleSpecs = baubles.map((d) => setupBaubles(d, tree));
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<g>
|
|
111
|
+
{/* <defs>
|
|
112
|
+
<clipPath id="clip">
|
|
113
|
+
<rect x={-margins.left} y={-margins.top} width={width} height={height} />
|
|
114
|
+
</clipPath>
|
|
115
|
+
</defs> */}
|
|
116
|
+
{/*<rect x="0" y="0" width="100%" height="100%" fill="none" pointerEvents={"visible"} onClick={()=>nodeDispatch({type:"clearSelection"})}/>*/}
|
|
117
|
+
{/* <g transform={`translate(${margins.left},${margins.top})`} clipPath={'url(#clip)'} > */}
|
|
118
|
+
<g transform={`translate(${x},${y})`}>
|
|
119
|
+
<ScaleContext.Provider value={scale}>
|
|
120
|
+
<DimensionContext.Provider value={dimensions}>
|
|
121
|
+
<layoutContext.Provider value={layoutMap}>
|
|
122
|
+
<animatedContext.Provider value={animated}>
|
|
123
|
+
{axis ? <Axis {...axis} /> : null}
|
|
124
|
+
{baubleSpecs.map((specs, i) => (
|
|
125
|
+
<Bauble key={specs.id ?? i} {...specs} />
|
|
126
|
+
))}
|
|
127
|
+
</animatedContext.Provider>
|
|
128
|
+
</layoutContext.Provider>
|
|
129
|
+
</DimensionContext.Provider>
|
|
130
|
+
</ScaleContext.Provider>
|
|
131
|
+
</g>
|
|
132
|
+
</g>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export default FigTree;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import React, { useContext } from "react";
|
|
2
|
+
import type { NodeRef } from "../../evo";
|
|
3
|
+
|
|
4
|
+
import { layoutClass } from "../../layouts";
|
|
5
|
+
import { normalizePath } from "../../path.helpers";
|
|
6
|
+
import type { PolarVertex, simpleVertex } from "../../layouts/types";
|
|
7
|
+
import { ScaleContext } from "../../context/scale-context";
|
|
8
|
+
import { layoutContext } from "../../context/layout-context";
|
|
9
|
+
import { animatedContext } from "../../context/aminated-context";
|
|
10
|
+
import type { PathAttrs } from "../baubles/shapes/branch";
|
|
11
|
+
|
|
12
|
+
//The goal here is now to take a shape components that accepts Attrs: number | string , x/y
|
|
13
|
+
// and return a component that takes a node / layout/ scale and attrs:number|string | function
|
|
14
|
+
|
|
15
|
+
type Injected = {
|
|
16
|
+
d: string;
|
|
17
|
+
animated: boolean;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* This HOC takes a shape (possibly animated) that requires d values and calculated attributes and
|
|
22
|
+
* calculates those values from a node and its parent.
|
|
23
|
+
*
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
export function withBranch(WrappedComponent: React.FC<PathAttrs & Injected>) {
|
|
27
|
+
type ExposedProps = PathAttrs & {
|
|
28
|
+
node: NodeRef;
|
|
29
|
+
parent?: NodeRef;
|
|
30
|
+
curvature?: number;
|
|
31
|
+
};
|
|
32
|
+
// we will now calculate the x,y, attrs, and interactions
|
|
33
|
+
const BranchedComponent = (props: ExposedProps) => {
|
|
34
|
+
const scale = useContext(ScaleContext);
|
|
35
|
+
const layout = useContext(layoutContext);
|
|
36
|
+
const animated = useContext(animatedContext);
|
|
37
|
+
|
|
38
|
+
const { node, parent, curvature = 0, ...rest } = props;
|
|
39
|
+
|
|
40
|
+
const v = layout(node);
|
|
41
|
+
const { layoutClass } = v;
|
|
42
|
+
const p = parent ? layout(parent) : { x: v.x, y: v.y }; // straight line
|
|
43
|
+
const step = { x: p.x, y: v.y };
|
|
44
|
+
const points = [p, v, step].map((vertex) => scale(vertex));
|
|
45
|
+
const d = normalizePath(pathGenerator(points, curvature, layoutClass));
|
|
46
|
+
|
|
47
|
+
return <WrappedComponent d={d} animated={animated} {...rest} />;
|
|
48
|
+
};
|
|
49
|
+
BranchedComponent.displayName = `withBranchArray(${WrappedComponent.displayName || WrappedComponent.name || "Component"})`;
|
|
50
|
+
return BranchedComponent;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function pathGenerator(
|
|
54
|
+
points: simpleVertex[] | PolarVertex[],
|
|
55
|
+
curvature: number,
|
|
56
|
+
layout: layoutClass,
|
|
57
|
+
): string {
|
|
58
|
+
switch (layout) {
|
|
59
|
+
case layoutClass.Rectangular: {
|
|
60
|
+
return rectangularBranchPath(points, curvature);
|
|
61
|
+
}
|
|
62
|
+
case layoutClass.Polar: {
|
|
63
|
+
return polarBranchPath(points as PolarVertex[]);
|
|
64
|
+
}
|
|
65
|
+
case layoutClass.Radial: {
|
|
66
|
+
return radialBranchPath(points);
|
|
67
|
+
}
|
|
68
|
+
default: {
|
|
69
|
+
throw new Error(
|
|
70
|
+
`path generator not implemented for the ${layout as string} of points`,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
//TODO remove switch statement on number of positions that was for cartooned nodes that will be handled elsewhere
|
|
77
|
+
function rectangularBranchPath(
|
|
78
|
+
points: simpleVertex[],
|
|
79
|
+
curvature: number,
|
|
80
|
+
): string {
|
|
81
|
+
const positions = points.length;
|
|
82
|
+
|
|
83
|
+
switch (positions) {
|
|
84
|
+
case 0: {
|
|
85
|
+
return "";
|
|
86
|
+
}
|
|
87
|
+
case 3: {
|
|
88
|
+
const [parent, child] = points; //parent is parent and gets pushed to end of array
|
|
89
|
+
if (curvature === 0) {
|
|
90
|
+
// no curve
|
|
91
|
+
const x1 = parent.x + 0.001; // tiny adjustment for faded line (can't have y or x dimension not change at all
|
|
92
|
+
return `M${x1},${parent.y}L${parent.x},${child.y}L${child.x},${child.y + 0.001}`;
|
|
93
|
+
} else if (curvature < 1) {
|
|
94
|
+
// curve
|
|
95
|
+
return `M${parent.x},${parent.y}C${parent.x},${child.y}, ${parent.x + Math.abs(curvature * (parent.x - child.x))},${child.y} ${child.x},${child.y}`;
|
|
96
|
+
} else {
|
|
97
|
+
//(curvature == 1)
|
|
98
|
+
return `M${parent.x},${parent.y}L${(parent.x + child.x) / 2},${(parent.y + child.y) / 2}L${child.x},${child.y}`;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
default: {
|
|
102
|
+
throw new Error(
|
|
103
|
+
`path rectangular generator not implemented for this ${positions} of points`,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function polarBranchPath(points: PolarVertex[]): string {
|
|
110
|
+
const positions = points.length;
|
|
111
|
+
switch (positions) {
|
|
112
|
+
case 3: {
|
|
113
|
+
const [parent, child, step] = points;
|
|
114
|
+
const arcBit =
|
|
115
|
+
parent.theta === child.theta || parent.r === 0
|
|
116
|
+
? ""
|
|
117
|
+
: `A${parent.r},${parent.r} 0 0 ${parent.theta < child.theta ? 1 : 0} ${step.x},${step.y}`;
|
|
118
|
+
return `M${parent.x},${parent.y} ${arcBit} L${child.x},${child.y}`;
|
|
119
|
+
}
|
|
120
|
+
case 0: {
|
|
121
|
+
return "";
|
|
122
|
+
}
|
|
123
|
+
default: {
|
|
124
|
+
throw new Error(
|
|
125
|
+
`Error in polar path generator. not expecting ${positions} points`,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function radialBranchPath(points: simpleVertex[]): string {
|
|
132
|
+
const positions = points.length;
|
|
133
|
+
|
|
134
|
+
switch (positions) {
|
|
135
|
+
case 0: {
|
|
136
|
+
return "";
|
|
137
|
+
}
|
|
138
|
+
case 3: {
|
|
139
|
+
const [parent, child] = points; //parent is parent and gets pushed to end of array
|
|
140
|
+
return `M${parent.x},${parent.y}L${(parent.x + child.x) / 2},${(parent.y + child.y) / 2}L${child.x},${child.y}`;
|
|
141
|
+
}
|
|
142
|
+
default: {
|
|
143
|
+
throw new Error(
|
|
144
|
+
`path rectangular generator not implemented for this ${positions} of points`,
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An HOC that passes nodes and attrs to label components
|
|
3
|
+
*/
|
|
4
|
+
import type { NodeRef } from "../../evo";
|
|
5
|
+
import type { AttrsRecord, InternalInteractionType } from "../baubles/types";
|
|
6
|
+
|
|
7
|
+
export type BranchProps<A extends object> = {
|
|
8
|
+
branches: { node: NodeRef; parent: NodeRef }[];
|
|
9
|
+
attrs?: AttrsRecord<A>;
|
|
10
|
+
interactions?: { [key: string]: InternalInteractionType }; // keyed by node id // check type
|
|
11
|
+
// shape:NodeShapes,
|
|
12
|
+
keyBy?: (n: NodeRef) => string;
|
|
13
|
+
curvature?: number;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* A HOC that provides parent child and attrs to componets that need this information
|
|
18
|
+
*/
|
|
19
|
+
//TODO do interactions
|
|
20
|
+
export function withBranches<baseComponentAttrs extends object>(
|
|
21
|
+
ShapeComponent: React.FC<
|
|
22
|
+
baseComponentAttrs & { node: NodeRef; parent?: NodeRef; curvature?: number }
|
|
23
|
+
>,
|
|
24
|
+
): React.FC<BranchProps<baseComponentAttrs>> {
|
|
25
|
+
const withNodes = (props: BranchProps<baseComponentAttrs>) => {
|
|
26
|
+
const {
|
|
27
|
+
branches,
|
|
28
|
+
keyBy = (n: NodeRef) => n._id,
|
|
29
|
+
attrs = {},
|
|
30
|
+
curvature,
|
|
31
|
+
} = props;
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<g className={"branch-layer"}>
|
|
35
|
+
{branches.map(({ node, parent }) => {
|
|
36
|
+
const nodeAttrs = attrs[node._id] ?? {};
|
|
37
|
+
// const nodeInteractions = interactions[node._id]?? {}
|
|
38
|
+
return (
|
|
39
|
+
<ShapeComponent
|
|
40
|
+
key={keyBy(node)}
|
|
41
|
+
node={node}
|
|
42
|
+
parent={parent}
|
|
43
|
+
{...nodeAttrs}
|
|
44
|
+
curvature={curvature}
|
|
45
|
+
// interactions={nodeInteractions}
|
|
46
|
+
/>
|
|
47
|
+
);
|
|
48
|
+
})}
|
|
49
|
+
</g>
|
|
50
|
+
);
|
|
51
|
+
};
|
|
52
|
+
withNodes.displayName = `withBranchArray(${ShapeComponent.displayName || ShapeComponent.name || "Component"})`;
|
|
53
|
+
return withNodes;
|
|
54
|
+
}
|