@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,96 @@
1
+ import { layoutClass } from "../../../layouts";
2
+ import { arc as arcgen } from "d3-shape";
3
+ import type { Clade } from "../../hoc/with-clades";
4
+ import { withClades } from "../../hoc/with-clades";
5
+
6
+ import { BasePath, BaseRectangle } from "../shapes";
7
+ import type { PolarVertex, simplePolarVertex } from "../../../layouts/types";
8
+ import { notNull } from "../../../utils";
9
+ import { ScaleContext } from "../../../context/scale-context";
10
+ import { layoutContext } from "../../../context/layout-context";
11
+ import { animatedContext } from "../../../context/aminated-context";
12
+ import { useContext } from "react";
13
+
14
+ const arc = arcgen();
15
+ //TODO add padding
16
+ // const padding = 10;
17
+ /**
18
+ * A highlight around a clade of interest.
19
+ * For Polar layouts this will be a shaded arc.
20
+ * In a rectangular figure this will be a rectangle around the clade
21
+ */
22
+
23
+ type BaseHighlightAttrs = {
24
+ d?: string;
25
+ x?: number;
26
+ y?: number;
27
+ width?: number;
28
+ height?: number;
29
+ transform?: string;
30
+ stroke?: string;
31
+ stokeWidth?: number;
32
+ fill?: string;
33
+ animated: boolean;
34
+ };
35
+
36
+ type Injected = {
37
+ d?: string;
38
+ x?: number;
39
+ y?: number;
40
+ width?: number;
41
+ height?: number;
42
+ transform?: string;
43
+ animated: boolean;
44
+ };
45
+ type HighlightAttrs = Omit<BaseHighlightAttrs, keyof Injected>;
46
+
47
+ function Highlight(props: HighlightAttrs & { clade: Clade }) {
48
+ const { clade, ...rest } = props;
49
+ const scale = useContext(ScaleContext);
50
+ const layout = useContext(layoutContext);
51
+ const animated = useContext(animatedContext);
52
+
53
+ const { root, leftMost, rightMost, mostDiverged } = clade;
54
+ const v = scale(layout(root));
55
+ const lmv = scale(layout(leftMost)); // left most child v (top of highlight)
56
+ const rmv = scale(layout(rightMost)); // right most child v (top of highlight)
57
+ const mdv = scale(layout(mostDiverged)); // right most child v (top of highlight)
58
+ const { layoutClass: layoutType } = layout(root);
59
+ if (layoutType === layoutClass.Rectangular) {
60
+ const width = mdv.x - v.x;
61
+ const height = Math.abs(lmv.y - rmv.y);
62
+ return (
63
+ <BaseRectangle
64
+ width={width}
65
+ height={height}
66
+ x={v.x}
67
+ y={Math.min(lmv.y, rmv.y)}
68
+ {...rest}
69
+ animated={animated}
70
+ />
71
+ );
72
+ } else if (layoutType === layoutClass.Polar) {
73
+ // if we are here then scale returned a polar vertex
74
+ const origin = scale({ x: 0, y: 0 }) as simplePolarVertex;
75
+ const transform = `translate(${origin.x},${origin.y})`;
76
+ const minR = (v as PolarVertex).r; //padding?
77
+ const maxR = (mdv as PolarVertex).r;
78
+ const maxTheta = (lmv as PolarVertex).theta;
79
+ const minTheta = (rmv as PolarVertex).theta;
80
+ const shape = arc({
81
+ innerRadius: minR,
82
+ outerRadius: maxR + 5,
83
+ startAngle: minTheta + Math.PI / 2,
84
+ endAngle: maxTheta + Math.PI / 2,
85
+ });
86
+ notNull(shape, `Error making arc shape for Clade Highlight`);
87
+
88
+ return (
89
+ <BasePath d={shape} transform={transform} {...rest} animated={animated} />
90
+ ); //transform={transform}
91
+ } else {
92
+ return null;
93
+ }
94
+ }
95
+
96
+ export const Highlights = withClades(Highlight);
@@ -0,0 +1 @@
1
+ // exporting for public use
@@ -0,0 +1,45 @@
1
+ import type { BaubleTarget } from "./bauble";
2
+ import { CladeShapes } from "./bauble";
3
+
4
+ import type { InternalInteractionType } from "./types";
5
+ import type { NodeRef } from "../../evo";
6
+ import { Cartoons } from "./clades/cartoon";
7
+ import { Highlights } from "./clades/highlight";
8
+ import type { Clade } from "../hoc/with-clades";
9
+ import type { PathAttrs, RectAttrs } from "./shapes";
10
+
11
+ // Accessor for Node Shapes
12
+
13
+ export function Clades(props: CladeProps) {
14
+ switch (props.shape) {
15
+ case CladeShapes.Cartoon:
16
+ return <Cartoons {...props} />;
17
+ case CladeShapes.Highlight:
18
+ return <Highlights {...props} />;
19
+ }
20
+ }
21
+
22
+ type CartoonCladeProps = {
23
+ shape: CladeShapes.Cartoon;
24
+ clades: Clade[];
25
+ attrs: { [key: string]: PathAttrs };
26
+ interactions?: { [key: string]: InternalInteractionType }; // keyed by node id // check type
27
+ // shape:NodeShapes,
28
+ keyBy?: (n: NodeRef) => string;
29
+ };
30
+ type HighlighCladeProps = {
31
+ shape: CladeShapes.Highlight;
32
+ clades: Clade[];
33
+ attrs: { [key: string]: Omit<RectAttrs, "width" | "height"> };
34
+ interactions?: { [key: string]: InternalInteractionType }; // keyed by node id // check type
35
+ // shape:NodeShapes,
36
+ keyBy?: (n: NodeRef) => string;
37
+ };
38
+
39
+ export type HighlightRectAttrs = Omit<RectAttrs, "width" | "height">; //width and height provided by figtree
40
+ export type CladeProps = CartoonCladeProps | HighlighCladeProps;
41
+
42
+ export type CladeSpec = CladeProps & {
43
+ target: BaubleTarget.Clade;
44
+ id?: string;
45
+ };
@@ -0,0 +1,62 @@
1
+ import type { NodeRef } from "../../evo";
2
+ import type { AttrAndInteractionApplier, Attrs, Fn, LiftToUser } from "./types";
3
+
4
+ export function isFn(x: unknown): x is Fn {
5
+ return typeof x === "function";
6
+ }
7
+
8
+ function mapAttrsToProps<A extends Attrs>(
9
+ attrs: LiftToUser<A>,
10
+ ): (n: NodeRef) => A {
11
+ return function (node: NodeRef) {
12
+ const props = {} as Record<keyof A, number | string>;
13
+ for (const key in attrs) {
14
+ const k = key as keyof A;
15
+ const v = attrs[k];
16
+ props[k] = isFn(v) ? (v as Fn)(node) : (v as number | string); // eslint-disable-line @typescript-eslint/no-unnecessary-type-assertion
17
+ }
18
+ return props as A;
19
+ };
20
+ }
21
+
22
+ function applyInteractions(
23
+ interactions: Record<string, (n: NodeRef) => void>,
24
+ ): (n: NodeRef) => Record<string, () => void> {
25
+ return function (node: NodeRef) {
26
+ const props: Record<string, () => void> = {};
27
+ for (const [key, value] of Object.entries(interactions)) {
28
+ props[key] = () => {
29
+ value(node);
30
+ }; // wraps base interaction
31
+ }
32
+ return props;
33
+ };
34
+ }
35
+
36
+ /** A function that takes user provided attrs and interactions and returns a function
37
+ * that takes a node and returns the interaction and Attrs in the form expected by baubles
38
+ */
39
+ export function useAttributeMappers<A extends Attrs>(
40
+ attrs: LiftToUser<A>,
41
+ interactions?: Record<string, (n: NodeRef) => void>,
42
+ ): AttrAndInteractionApplier<A> {
43
+ // const { attrs, interactions} = props;
44
+
45
+ //This memorizes the functions so they are not made each time - maybe overkill.
46
+ // const baseAttrMapper = useCallback(mapAttrsToProps((attrs?attrs:{})), [attrs]);
47
+ // const baseInteractionMapper = useCallback(applyInteractions((interactions?interactions:{})), [interactions]);
48
+ // const tooltipMapper = useCallback(mapAttrsToProps((tooltip?tooltip:{})),[tooltip]);
49
+
50
+ const baseAttrMapper = mapAttrsToProps(attrs);
51
+
52
+ const baseInteractionMapper = interactions
53
+ ? applyInteractions(interactions)
54
+ : () => undefined;
55
+
56
+ return function shapeProps(node: NodeRef) {
57
+ return {
58
+ attrs: baseAttrMapper(node),
59
+ interactions: baseInteractionMapper(node),
60
+ };
61
+ };
62
+ }
@@ -0,0 +1,16 @@
1
+ // public exports
2
+ //TODO only expose <baubles />
3
+
4
+ // pass on lower imports
5
+ export * from "./clades";
6
+ export * from "./shapes";
7
+
8
+ // exports from this directory
9
+ export { Branches } from "./branches";
10
+ export type { BranchSpec } from "./branches";
11
+
12
+ export { Nodes } from "./nodes";
13
+ export type { NodeSpec } from "./nodes";
14
+
15
+ export { NodeLabels, BranchLabels } from "./labels"; // Todo unify to Label
16
+ export type { LabelSpec } from "./labels";
@@ -0,0 +1,38 @@
1
+ import type { NodeRef } from "../../evo";
2
+ import { withBranches } from "../hoc/with-branches";
3
+ import { withNodeLabel } from "../hoc/with-node";
4
+ import { withNodes } from "../hoc/with-nodes";
5
+ import type { BaubleTarget } from "./bauble";
6
+ import { BaseLabel } from "./shapes";
7
+ import type { TextAttrs } from "./shapes/label";
8
+ import type { AttrsRecord } from "./types";
9
+
10
+ export const BranchLabels = withBranches<TextAttrs>(
11
+ withNodeLabel<TextAttrs>(BaseLabel),
12
+ );
13
+ export const NodeLabels = withNodes<TextAttrs>(
14
+ withNodeLabel<TextAttrs>(BaseLabel),
15
+ );
16
+
17
+ export type NodeLabelProps = {
18
+ id?: string;
19
+ nodes: NodeRef[];
20
+ attrs: AttrsRecord<TextAttrs>;
21
+ aligned: boolean;
22
+ };
23
+ export type NodeLabelSpec = NodeLabelProps & {
24
+ target: BaubleTarget.NodeLabel;
25
+ id?: string;
26
+ };
27
+
28
+ export type BranchLabelProps = {
29
+ id?: string;
30
+ branches: { node: NodeRef; parent: NodeRef }[];
31
+ attrs: AttrsRecord<TextAttrs>;
32
+ };
33
+ export type BranchLabelSpec = BranchLabelProps & {
34
+ target: BaubleTarget.BranchLabel;
35
+ id?: string;
36
+ };
37
+
38
+ export type LabelSpec = BranchLabelSpec | NodeLabelSpec;
@@ -0,0 +1,51 @@
1
+ import type { CircleAttrs, RectAttrs } from "./shapes";
2
+ import { BaseCircle, CenteredRectangle } from "./shapes";
3
+
4
+ import { withNode } from "../hoc";
5
+ import type { BaubleTarget } from "./bauble";
6
+ import { NodeShapes } from "./bauble";
7
+ import { withNodes } from "../hoc/with-nodes";
8
+ import type { InternalInteractionType } from "./types";
9
+ import type { NodeRef } from "../../evo";
10
+
11
+ /**
12
+ * Add a Circle node Bauble to the figure.
13
+ */
14
+ const CircleNodes = withNodes(withNode(BaseCircle));
15
+ /**
16
+ *
17
+ * Add a Rectangular node Bauble to the figure.
18
+ */
19
+ const RectangleNodes = withNodes(withNode(CenteredRectangle));
20
+
21
+ // Accessor for Node Shapes
22
+
23
+ export function Nodes(props: NodeProps) {
24
+ switch (props.shape) {
25
+ case NodeShapes.Circle:
26
+ return <CircleNodes {...props} />;
27
+ case NodeShapes.Rectangle:
28
+ return <RectangleNodes {...props} />;
29
+ }
30
+ }
31
+
32
+ type CircleNodeProps = {
33
+ shape: NodeShapes.Circle;
34
+ nodes: NodeRef[];
35
+ attrs: { [key: string]: CircleAttrs };
36
+ interactions?: { [key: string]: InternalInteractionType }; // keyed by node id // check type
37
+ // shape:NodeShapes,
38
+ keyBy?: (n: NodeRef) => string;
39
+ };
40
+ type RectangleNodeProps = {
41
+ shape: NodeShapes.Rectangle;
42
+ nodes: NodeRef[];
43
+ attrs: { [key: string]: RectAttrs };
44
+ interactions?: { [key: string]: InternalInteractionType }; // keyed by node id // check type
45
+ // shape:NodeShapes,
46
+ keyBy?: (n: NodeRef) => string;
47
+ };
48
+
49
+ type NodeProps = CircleNodeProps | RectangleNodeProps;
50
+
51
+ export type NodeSpec = NodeProps & { target: BaubleTarget.Node; id?: string };
@@ -0,0 +1,53 @@
1
+ import type { AnimatedProps } from "@react-spring/web";
2
+ import { animated, useSpring } from "@react-spring/web";
3
+
4
+ // update ref to work with animated expectations no old refs
5
+ export type BasePathDOMProps = Omit<React.ComponentProps<"path">, "ref"> & {
6
+ ref?: React.Ref<SVGPathElement>;
7
+ };
8
+ export type PathAttrs = Omit<BasePathDOMProps, "d">; // d will be calculated
9
+
10
+ export type PathProps = PathAttrs & { d: string; animated: boolean };
11
+
12
+ /** React component that renders a path.
13
+ * The fill is defaulted to 'none' but is overwritten by anything in attrs
14
+ */
15
+
16
+ export const animatableBranchKeys = ["d", "stroke", "strokeWidth"] as const;
17
+
18
+ type AnimKey = (typeof animatableBranchKeys)[number];
19
+
20
+ // attribute get spread here interactions come in
21
+ function pickAnimatable(
22
+ props: Record<string, unknown>,
23
+ ): Partial<Record<AnimKey, number | string>> {
24
+ const out: Partial<Record<AnimKey, number | string>> = {};
25
+ for (const k of animatableBranchKeys) {
26
+ const v = props[k];
27
+ // Keep 0; only exclude null/undefined
28
+ if (v != null && (typeof v === "number" || typeof v === "string")) {
29
+ out[k] = v;
30
+ }
31
+ }
32
+ return out;
33
+ }
34
+
35
+ export function BasePath(props: PathProps) {
36
+ const { animated: a, ...rest } = props;
37
+ const aAttrs = pickAnimatable(rest);
38
+ // Keep hooks count constant
39
+ const animatedValues = useSpring({
40
+ ...aAttrs,
41
+ config: { duration: 500 },
42
+ });
43
+ if (!a) {
44
+ return <animated.path {...rest} />;
45
+ } else {
46
+ return (
47
+ <animated.path
48
+ {...rest}
49
+ {...(animatedValues as AnimatedProps<BasePathDOMProps>)}
50
+ />
51
+ );
52
+ }
53
+ }
@@ -0,0 +1,64 @@
1
+ import type { AnimatedProps } from "@react-spring/web";
2
+ import { animated, useSpring } from "@react-spring/web";
3
+
4
+ // update ref to work with animated expectations no old refs
5
+ type BaseCircleDOMProps = Omit<React.ComponentProps<"circle">, "ref"> & {
6
+ ref?: React.Ref<SVGCircleElement>;
7
+ };
8
+ export type CircleAttrs = Omit<BaseCircleDOMProps, "cx" | "cy"> & { r: number };
9
+ export type CircleProps = CircleAttrs & {
10
+ x: number;
11
+ y: number;
12
+ animated: boolean;
13
+ };
14
+ export const animatableCircleKeys = [
15
+ "cx",
16
+ "cy",
17
+ "r",
18
+ "stroke",
19
+ "strokeWidth",
20
+ "fill",
21
+ ] as const;
22
+
23
+ type AnimKey = (typeof animatableCircleKeys)[number];
24
+
25
+ // attribute get spread here interactions come in
26
+ function pickAnimatable(
27
+ props: Record<string, unknown>,
28
+ ): Partial<Record<AnimKey, number | string>> {
29
+ const out: Partial<Record<AnimKey, number | string>> = {};
30
+ for (const k of animatableCircleKeys) {
31
+ const v = props[k];
32
+ // Keep 0; only exclude null/undefined
33
+ if (v != null && (typeof v === "number" || typeof v === "string")) {
34
+ out[k] = v;
35
+ }
36
+ }
37
+ return out;
38
+ }
39
+
40
+ /** React component that renders a circle */
41
+ export const BaseCircle = function (props: CircleProps) {
42
+ const { animated: a, x, y, ...attrs } = props;
43
+ // maybe could put animation logic in an HOC but that's causing type issues
44
+ const mappedProps = { ...attrs, cx: x, cy: y };
45
+ const aAttrs = pickAnimatable(mappedProps);
46
+ // Keep hooks count constant
47
+ const animatedValues = useSpring({
48
+ ...aAttrs,
49
+ config: { duration: 500 },
50
+ });
51
+ if (!a) {
52
+ return <animated.circle className={"node-shape"} {...mappedProps} />;
53
+ } else {
54
+ return (
55
+ <animated.circle
56
+ className={"node-shape"}
57
+ {...mappedProps}
58
+ {...(animatedValues as AnimatedProps<BaseCircleDOMProps>)}
59
+ />
60
+ );
61
+ }
62
+ };
63
+
64
+ // export const AnimatedCircle = withAnimation(BaseCircle)
@@ -0,0 +1,9 @@
1
+ // Package exports
2
+ export type { CircleAttrs } from "./circle";
3
+ export { BaseCircle } from "./circle";
4
+ export type { PathAttrs } from "./branch";
5
+ export { BasePath } from "./branch";
6
+ export type { RectAttrs } from "./rectangle";
7
+ export { BaseRectangle, CenteredRectangle } from "./rectangle";
8
+ export type { TextAttrs } from "./label";
9
+ export { BaseLabel } from "./label";
@@ -0,0 +1,104 @@
1
+ import type { SpringValue, Interpolation } from "@react-spring/web";
2
+ import { to, animated, useSpring } from "@react-spring/web";
3
+ import type { numerical } from "../types";
4
+ import { isSpringNumber } from "../types";
5
+ import type { LabelInjection } from "../../hoc/with-node";
6
+
7
+ /**
8
+ * Label attributes for styling and rendering labels.
9
+ * These will be stripped and trickle up to the user
10
+ */
11
+
12
+ type BaseTextDOMProps = Omit<React.ComponentProps<"text">, "ref"> & {
13
+ ref?: React.Ref<SVGTextElement>;
14
+ };
15
+ export type TextAttrs = BaseTextDOMProps & { text: string };
16
+ type LabelProps = TextAttrs & LabelInjection; // need these to match injected exactly for type inference
17
+
18
+ /** The props needed for rendering a label in the svg*/
19
+
20
+ // transform={to([animatedProperties.x, animatedProperties.y, animatedProperties.rotation], (x, y, rotation) => `translate(${x},${y}) rotate(${rotation})`)}
21
+ function getTransform(
22
+ x: numerical,
23
+ y: numerical,
24
+ rotation: numerical,
25
+ ): string | SpringValue<string> | Interpolation<string> {
26
+ if (isSpringNumber(x) || isSpringNumber(y) || isSpringNumber(rotation)) {
27
+ return to(
28
+ [x, y, rotation],
29
+ (x, y, rotation) => `translate(${x},${y}) rotate(${rotation})`,
30
+ );
31
+ }
32
+ return `translate(${x},${y}) rotate(${rotation})`;
33
+ }
34
+
35
+ export function BaseLabel(props: LabelProps) {
36
+ const {
37
+ alignmentBaseline,
38
+ textAnchor,
39
+ rotation,
40
+ x,
41
+ y,
42
+ text,
43
+ d,
44
+ animated: a,
45
+ ...other
46
+ } = props;
47
+
48
+ const animatedValues = useSpring({
49
+ x,
50
+ y,
51
+ rotation,
52
+ config: { duration: 500 },
53
+ });
54
+
55
+ if (!a) {
56
+ const transform = getTransform(x, y, rotation);
57
+ return (
58
+ <g>
59
+ <animated.text
60
+ alignmentBaseline={alignmentBaseline}
61
+ textAnchor={textAnchor}
62
+ transform={transform}
63
+ {...other}
64
+ >
65
+ {text}
66
+ </animated.text>
67
+ {d ? (
68
+ <animated.path
69
+ strokeWidth={1}
70
+ stroke="grey"
71
+ strokeDasharray="2"
72
+ d={d}
73
+ />
74
+ ) : null}
75
+ </g>
76
+ );
77
+ } else {
78
+ const animatedTransform = getTransform(
79
+ animatedValues.x,
80
+ animatedValues.y,
81
+ animatedValues.rotation,
82
+ );
83
+ return (
84
+ <g>
85
+ <animated.text
86
+ alignmentBaseline={alignmentBaseline}
87
+ textAnchor={textAnchor}
88
+ transform={animatedTransform}
89
+ {...other}
90
+ >
91
+ {text}
92
+ </animated.text>
93
+ {d ? (
94
+ <animated.path
95
+ strokeWidth={1}
96
+ stroke="grey"
97
+ strokeDasharray="2"
98
+ d={d}
99
+ />
100
+ ) : null}
101
+ </g>
102
+ );
103
+ }
104
+ }
@@ -0,0 +1,83 @@
1
+ import type { AnimatedProps } from "@react-spring/web";
2
+ import { animated, useSpring } from "@react-spring/web";
3
+
4
+ /** Find where to put a position so the center is the provided location
5
+ * used below for centered rectangles. Nodes provide the x,y of the center.
6
+ */
7
+ function centerNumber(pos: number, size: number): number {
8
+ // plain numbers
9
+ return pos - size / 2;
10
+ }
11
+
12
+ /**
13
+ * Rectangle attributes for styling and rendering a rectangle.
14
+ * These will be stripped and trickle up to the user
15
+ */
16
+ // update ref to work with animated expectations no old refs
17
+ type BaseRectangleDOMProps = Omit<React.ComponentProps<"rect">, "ref"> & {
18
+ ref?: React.Ref<SVGRectElement>;
19
+ };
20
+ export type RectAttrs = BaseRectangleDOMProps & {
21
+ width: number;
22
+ height: number;
23
+ };
24
+ export type RectProps = RectAttrs & { animated: boolean; x: number; y: number }; // need these to match injected exactly for type inference
25
+ export const animatableRectKeys = [
26
+ "rx",
27
+ "ry",
28
+ "x",
29
+ "y",
30
+ "width",
31
+ "height",
32
+ "stroke",
33
+ "strokeWidth",
34
+ ] as const;
35
+
36
+ type AnimKey = (typeof animatableRectKeys)[number];
37
+
38
+ // attribute get spread here interactions come in
39
+ //TODO make this a generalized function
40
+ function pickAnimatable(
41
+ props: Record<string, unknown>,
42
+ ): Partial<Record<AnimKey, number | string>> {
43
+ const out: Partial<Record<AnimKey, number | string>> = {};
44
+ for (const k of animatableRectKeys) {
45
+ const v = props[k];
46
+ // Keep 0; only exclude null/undefined
47
+ if (v != null && (typeof v === "number" || typeof v === "string")) {
48
+ out[k] = v;
49
+ }
50
+ }
51
+ return out;
52
+ }
53
+
54
+ /** A Rectangle centered on the provided x,y */
55
+ export const CenteredRectangle = function (props: RectProps) {
56
+ const { x, y, width, height, ...rest } = props;
57
+ const xCentered = centerNumber(x, width);
58
+ const yCentered = centerNumber(y, height);
59
+ const newAttrs = { ...rest, x: xCentered, y: yCentered, width, height };
60
+ return <BaseRectangle {...newAttrs} />;
61
+ };
62
+ /**
63
+ * A rectangle rendered as expected in an svg
64
+ */
65
+ export const BaseRectangle = function (props: RectProps) {
66
+ const { animated: a, ...attrs } = props;
67
+ const aAttrs = pickAnimatable(attrs);
68
+ // Keep hooks count constant
69
+ const animatedValues = useSpring({
70
+ ...aAttrs,
71
+ config: { duration: 500 },
72
+ });
73
+ if (!a) {
74
+ return <animated.rect className={"node-shape"} {...attrs} />;
75
+ }
76
+ return (
77
+ <animated.rect
78
+ className={"node-shape"}
79
+ {...attrs}
80
+ {...(animatedValues as AnimatedProps<BaseRectangleDOMProps>)}
81
+ />
82
+ );
83
+ };