@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,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,2 @@
1
+ export * from "./axis";
2
+ export * from "./legend";
@@ -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,3 @@
1
+ export { default as FigTree } from "./figtree";
2
+ export type { FigtreeProps } from "./figtree-types";
3
+ export type { BaubleTypes } from "./figtree-types";
@@ -0,0 +1,7 @@
1
+ //Internal exports
2
+
3
+ export { withNode } from "./with-node";
4
+ export { withNodes } from "./with-nodes";
5
+ export { withBranch } from "./with-branch";
6
+ export { withBranches } from "./with-branches";
7
+ export { withClades } from "./with-clades";
@@ -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
+ }