@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,123 @@
1
+ import { describe, test } from "vitest";
2
+ import { render } from "@testing-library/react";
3
+ import { ImmutableTree } from "../../evo";
4
+ import { FigTree } from "../../components";
5
+ import { rectangularLayout } from "../../layouts";
6
+ import {
7
+ Branches,
8
+ CircleNodes,
9
+ RectangleNodes,
10
+ } from "../../bauble-makers/makers";
11
+
12
+ const tree = ImmutableTree.fromNewick("((A:1,B:1):2,C:1);");
13
+
14
+ describe("Figures", () => {
15
+ test("renders a simple figure", () => {
16
+ const fig = render(
17
+ <svg
18
+ width="400px"
19
+ height="400px"
20
+ data-testid="figure"
21
+ xmlns="http://www.w3.org/2000/svg"
22
+ >
23
+ <FigTree
24
+ width={400}
25
+ height={400}
26
+ tree={tree}
27
+ layout={rectangularLayout}
28
+ />
29
+ </svg>,
30
+ );
31
+ //fig.debug();
32
+ expect(fig).toMatchSnapshot();
33
+ // expect(fig).toBeInTheDocument();
34
+ });
35
+ test("renders a tree with branch baubles", () => {
36
+ const tree = ImmutableTree.fromNewick("((A:1,B:1):2,C:1);");
37
+ const fig = render(
38
+ <svg
39
+ width="400px"
40
+ height="400px"
41
+ data-testid="figure"
42
+ xmlns="http://www.w3.org/2000/svg"
43
+ >
44
+ <FigTree
45
+ width={400}
46
+ height={400}
47
+ tree={tree}
48
+ layout={rectangularLayout}
49
+ baubles={[Branches({ attrs: { strokeWidth: 2, stroke: "black" } })]}
50
+ />
51
+ </svg>,
52
+ );
53
+ //fig.debug();
54
+ expect(fig).toMatchSnapshot();
55
+ });
56
+
57
+ test("renders a tree with branch baubles and nodes", () => {
58
+ const tree = ImmutableTree.fromNewick("((A:1,B:1):2,C:1);");
59
+ const fig = render(
60
+ <svg
61
+ width="400px"
62
+ height="400px"
63
+ data-testid="figure"
64
+ xmlns="http://www.w3.org/2000/svg"
65
+ >
66
+ <FigTree
67
+ width={400}
68
+ height={400}
69
+ tree={tree}
70
+ layout={rectangularLayout}
71
+ baubles={[
72
+ Branches({ attrs: { strokeWidth: 2, stroke: "black" } }),
73
+ CircleNodes({
74
+ filter: (n) => tree.isExternal(n),
75
+ attrs: { fill: "black", r: 10 },
76
+ }),
77
+ RectangleNodes({
78
+ filter: (n) => !tree.isExternal(n),
79
+ attrs: { fill: "black", width: 10, height: 4 },
80
+ }),
81
+ ]}
82
+ />
83
+ </svg>,
84
+ );
85
+ //fig.debug();
86
+ expect(fig).toMatchSnapshot();
87
+ // screen.debug()
88
+ // expect(fig).toBeInTheDocument();
89
+ });
90
+
91
+ test("renders a tree with branch baubles and and functions", () => {
92
+ const tree = ImmutableTree.fromNewick("((A:1,B:1):2,C:1);");
93
+ const fig = render(
94
+ <svg
95
+ width="400px"
96
+ height="400px"
97
+ data-testid="figure"
98
+ xmlns="http://www.w3.org/2000/svg"
99
+ >
100
+ <FigTree
101
+ width={400}
102
+ height={400}
103
+ tree={tree}
104
+ layout={rectangularLayout}
105
+ baubles={[
106
+ Branches({ attrs: { strokeWidth: 2, stroke: () => "black" } }),
107
+ CircleNodes({
108
+ filter: (n) => tree.isExternal(n),
109
+ attrs: { fill: "black", r: (n) => n.number },
110
+ }),
111
+ RectangleNodes({
112
+ filter: (n) => !tree.isExternal(n),
113
+ attrs: { fill: "black", width: 10, height: 4 },
114
+ }),
115
+ ]}
116
+ />
117
+ </svg>,
118
+ );
119
+ //fig.debug();
120
+ expect(fig).toMatchSnapshot();
121
+ // expect(fig).toBeInTheDocument();
122
+ });
123
+ });
@@ -0,0 +1,47 @@
1
+ <svg
2
+ data-testid="figure"
3
+ height="400px"
4
+ width="400px"
5
+ xmlns="http://www.w3.org/2000/svg"
6
+ >
7
+ <g>
8
+ <g
9
+ transform="translate(10,10)"
10
+ >
11
+ <g
12
+ class="branch-layer"
13
+ >
14
+ <path
15
+ d="M 0.001 237.5 C0.001,237.5 0,237.5 0,237.5 C0,237.5 0,237.5005 0,237.50074999999998 C0,237.50074999999998 0,237.500875 0,237.50093750000002 C0,237.50093750000002 0,237.50096875000003 0,237.50098437500003 C0,237.50098437500003 0,237.5009921875 0,237.50099609375002 C0,237.50099609375002 0,237.501 0,237.501 "
16
+ fill="none"
17
+ stroke="black"
18
+ stroke-width="1"
19
+ />
20
+ <path
21
+ d="M 0.001 237.5 C0.001,237.5 0,95 0,95 C0,95 126.66666666666666,95.0005 190,95.00075000000001 C190,95.00075000000001 221.66666666666666,95.00087500000001 237.5,95.0009375 C237.5,95.0009375 245.41666666666666,95.00096875 249.375,95.000984375 C249.375,95.000984375 251.35416666666666,95.0009921875 252.34375,95.00099609375 C252.34375,95.00099609375 253.33333333333331,95.001 253.33333333333331,95.001 "
22
+ fill="none"
23
+ stroke="black"
24
+ stroke-width="1"
25
+ />
26
+ <path
27
+ d="M 253.33433333333332 95 C253.33433333333332,95 253.33333333333331,0 253.33333333333331,0 C253.33333333333331,0 316.66666666666663,0.0005 348.3333333333333,0.00075 C348.3333333333333,0.00075 364.16666666666663,0.000875 372.0833333333333,0.0009375 C372.0833333333333,0.0009375 376.04166666666663,0.00096875 378.0208333333333,0.000984375 C378.0208333333333,0.000984375 379.01041666666663,0.0009921875 379.5052083333333,0.00099609375 C379.5052083333333,0.00099609375 380,0.001 380,0.001 "
28
+ fill="none"
29
+ stroke="black"
30
+ stroke-width="1"
31
+ />
32
+ <path
33
+ d="M 253.33433333333332 95 C253.33433333333332,95 253.33333333333331,190 253.33333333333331,190 C253.33333333333331,190 316.66666666666663,190.0005 348.3333333333333,190.00074999999998 C348.3333333333333,190.00074999999998 364.16666666666663,190.000875 372.0833333333333,190.00093750000002 C372.0833333333333,190.00093750000002 376.04166666666663,190.00096875000003 378.0208333333333,190.00098437500003 C378.0208333333333,190.00098437500003 379.01041666666663,190.0009921875 379.5052083333333,190.00099609375002 C379.5052083333333,190.00099609375002 380,190.001 380,190.001 "
34
+ fill="none"
35
+ stroke="black"
36
+ stroke-width="1"
37
+ />
38
+ <path
39
+ d="M 0.001 237.5 C0.001,237.5 0,380 0,380 C0,380 63.33333333333333,380.0005 95,380.00075 C95,380.00075 110.83333333333333,380.00087499999995 118.75,380.00093749999996 C118.75,380.00093749999996 122.70833333333333,380.00096874999997 124.6875,380.000984375 C124.6875,380.000984375 125.67708333333333,380.00099218749995 126.171875,380.00099609374996 C126.171875,380.00099609374996 126.66666666666666,380.001 126.66666666666666,380.001 "
40
+ fill="none"
41
+ stroke="black"
42
+ stroke-width="1"
43
+ />
44
+ </g>
45
+ </g>
46
+ </g>
47
+ </svg>
@@ -0,0 +1,23 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { ImmutableTree } from "../../evo";
3
+ import { radialLayout } from "../../layouts";
4
+ describe("Test rectangular layout", () => {
5
+ it("check x and y on root", function () {
6
+ const tree = ImmutableTree.fromNewick("((a:1,b:1):1,c:1);");
7
+ const layout = radialLayout(tree);
8
+ const root = layout(tree.getRoot());
9
+ expect(root.x).toBeCloseTo(0);
10
+ expect(root.y).toBeCloseTo(0);
11
+
12
+ // //a
13
+ const a = tree.getNodeByTaxon(tree.getTaxonByName("a"));
14
+ const aV = layout(a);
15
+ expect(aV.x).toBeCloseTo(0.0015820025368573631);
16
+ expect(aV.y).toBeCloseTo(-1.7311355093381278);
17
+
18
+ const c = tree.getNodeByTaxon(tree.getTaxonByName("c"));
19
+ const cV = layout(c);
20
+ expect(cV.x).toBeCloseTo(0.5);
21
+ expect(cV.y).toBeCloseTo(0.8660254037844386);
22
+ });
23
+ });
@@ -0,0 +1,65 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { ImmutableTree } from "../../evo";
3
+ import { rectangularLayout } from "../../layouts";
4
+ import { u } from "../../utils";
5
+
6
+ describe("Test rectangular layout", () => {
7
+ it("check x and y on root", function () {
8
+ const tree = ImmutableTree.fromNewick("((a:1,b:1):1,c:1);");
9
+ const layout = rectangularLayout(tree);
10
+ const root = layout(tree.getRoot());
11
+ expect(root.x).toBeCloseTo(0);
12
+ expect(root.y).toBeCloseTo(1.25);
13
+
14
+ // //a
15
+ const a = tree.getNodeByTaxon(tree.getTaxonByName("a"));
16
+ const aV = layout(a);
17
+ expect(aV.x).toBeCloseTo(2);
18
+ expect(aV.y).toBeCloseTo(0);
19
+
20
+ // //c
21
+ const c = tree.getNodeByTaxon(tree.getTaxonByName("c"));
22
+ const cV = layout(c);
23
+ expect(cV.x).toBeCloseTo(1);
24
+ expect(cV.y).toBeCloseTo(2);
25
+ });
26
+ it("check x and y on root - tree", function () {
27
+ const tree = ImmutableTree.fromNewick("((a:1,b:1):1,c:1);");
28
+
29
+ const layout = rectangularLayout(tree);
30
+ const root = layout(tree.getRoot());
31
+ const c = tree.getNodeByTaxon(tree.getTaxonByName("c"));
32
+ const cV = layout(c);
33
+ const tree2 = tree.setDivergence(
34
+ u(tree.getNodeByTaxon(tree.getTaxonByName("a"))),
35
+ 3.0,
36
+ );
37
+ const layout2 = rectangularLayout(tree2);
38
+ const root2 = layout2(tree2.getRoot());
39
+ const c2 = tree2.getNodeByTaxon(tree.getTaxonByName("c"));
40
+ const cV2 = layout2(c2);
41
+
42
+ expect(c2).toStrictEqual(c);
43
+ expect(cV2).toStrictEqual(cV);
44
+
45
+ expect(root2).not.toBe(root); //these are vertexes
46
+ expect(root2.x).toBeCloseTo(0);
47
+ });
48
+
49
+ it("check after reordering", function () {
50
+ // check above but with rotations
51
+
52
+ const tree = ImmutableTree.fromNewick("((a:1,b:1):1,c:1);");
53
+
54
+ const layout = rectangularLayout(tree);
55
+ const reordered = tree.orderNodesByDensity(true);
56
+ const reorderedLayout = rectangularLayout(reordered);
57
+ const reordededC = reordered.getNodeByTaxon(tree.getTaxonByName("c"));
58
+ const ogC = tree.getNodeByTaxon(tree.getTaxonByName("c"));
59
+
60
+ const ogCvertext = layout(ogC);
61
+ const reorderedCVertex = reorderedLayout(reordededC);
62
+
63
+ expect(ogCvertext).not.toBe(reorderedCVertex);
64
+ });
65
+ });
@@ -0,0 +1,40 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { render, screen } from "@testing-library/react";
3
+ import { BasePath } from "../../components/baubles/shapes";
4
+
5
+ describe("BaseBranch", () => {
6
+ test("renders BaseBranch, SVG path which has the right attributes", () => {
7
+ const d =
8
+ "M 10,30 A 20,20 0,0,1 50,30 A 20,20 0,0,1 90,30Q 90,60 50,90 Q 10,60 10,30 z";
9
+ render(
10
+ <svg>
11
+ <BasePath
12
+ d={d}
13
+ {...{ stroke: "blue", "data-testid": "branch", animated: false }}
14
+ />
15
+ </svg>,
16
+ );
17
+
18
+ const branch = screen.getByTestId("branch");
19
+ expect(branch.getAttribute("d")).toBe(d);
20
+ expect(branch.getAttribute("stroke")).toBe("blue");
21
+ });
22
+
23
+ test("Renders an animated branch , SVG path which has the right attributes", () => {
24
+ const d =
25
+ "M 10,30 A 20,20 0,0,1 50,30 A 20,20 0,0,1 90,30Q 90,60 50,90 Q 10,60 10,30 z";
26
+ render(
27
+ <svg>
28
+ <BasePath
29
+ d={d}
30
+ animated={true}
31
+ {...{ stroke: "blue", "data-testid": "branch" }}
32
+ />
33
+ </svg>,
34
+ );
35
+
36
+ const branch = screen.getByTestId("branch");
37
+ expect(branch.getAttribute("d")).toBe(d);
38
+ expect(branch.getAttribute("stroke")).toBe("rgba(0, 0, 255, 1)"); // animation used rga
39
+ });
40
+ });
@@ -0,0 +1,47 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { render, screen } from "@testing-library/react";
3
+ import { BaseCircle } from "../../components/baubles/shapes";
4
+
5
+ describe("BaseCircle", () => {
6
+ test("renders an SVG circle with the right attributes", () => {
7
+ render(
8
+ <svg>
9
+ <BaseCircle
10
+ x={0}
11
+ y={0}
12
+ {...{ r: 1, fill: "blue", "data-testid": "circle" }}
13
+ animated={false}
14
+ />
15
+ </svg>,
16
+ );
17
+
18
+ const circle = screen.getByTestId("circle"); // const circle = container.querySelector("circle");
19
+
20
+ expect(circle).not.toBeNull();
21
+ expect(circle.getAttribute("cx")).toBe("0");
22
+ expect(circle.getAttribute("cy")).toBe("0");
23
+ expect(circle.getAttribute("r")).toBe("1");
24
+ expect(circle.getAttribute("fill")).toBe("blue");
25
+ });
26
+
27
+ test("renders an animated circle with the right attributes", () => {
28
+ render(
29
+ <svg>
30
+ <BaseCircle
31
+ x={0}
32
+ y={0}
33
+ {...{ r: 1, fill: "blue", "data-testid": "circle" }}
34
+ animated={true}
35
+ />
36
+ </svg>,
37
+ );
38
+
39
+ const circle = screen.getByTestId("circle"); // const circle = container.querySelector("circle");
40
+
41
+ expect(circle).not.toBeNull();
42
+ expect(circle.getAttribute("cx")).toBe("0");
43
+ expect(circle.getAttribute("cy")).toBe("0");
44
+ expect(circle.getAttribute("r")).toBe("1");
45
+ expect(circle.getAttribute("fill")).toBe("rgba(0, 0, 255, 1)");
46
+ });
47
+ });
@@ -0,0 +1,101 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { render, screen } from "@testing-library/react";
3
+ import { BaseLabel } from "../../components/baubles/shapes/label";
4
+ import { unNullify } from "../../utils";
5
+
6
+ describe("BaseLabel", () => {
7
+ test("renders an SVG text element that uses transform to position", () => {
8
+ render(
9
+ <svg>
10
+ <BaseLabel
11
+ x={2}
12
+ y={2}
13
+ {...{ alignmentBaseline: "central", textAnchor: "end", rotation: 0 }}
14
+ text={"TEST"}
15
+ animated={false}
16
+ />
17
+ </svg>,
18
+ );
19
+ const textEl = screen.getByText("TEST");
20
+ expect(textEl.tagName.toLowerCase()).toBe("text");
21
+ expect(textEl.getAttribute("transform")).toBe("translate(2,2) rotate(0)");
22
+ });
23
+ test("renders an SVG text element an alignment line", () => {
24
+ const d =
25
+ "M 10,30 A 20,20 0,0,1 50,30 A 20,20 0,0,1 90,30Q 90,60 50,90 Q 10,60 10,30 z";
26
+ const { container } = render(
27
+ <svg>
28
+ <BaseLabel
29
+ x={2}
30
+ y={2}
31
+ {...{ alignmentBaseline: "central", textAnchor: "end", rotation: 0 }}
32
+ text={"TEST"}
33
+ animated={false}
34
+ d={d}
35
+ />
36
+ </svg>,
37
+ );
38
+
39
+ const path = unNullify(container.querySelector("path"), `path was null`);
40
+ expect(path).not.toBeNull();
41
+ expect(path.getAttribute("d")).toBe(d);
42
+ });
43
+
44
+ test("renders a label element an alignment line", () => {
45
+ const d =
46
+ "M 10,30 A 20,20 0,0,1 50,30 A 20,20 0,0,1 90,30Q 90,60 50,90 Q 10,60 10,30 z";
47
+ const { container } = render(
48
+ <svg>
49
+ <BaseLabel
50
+ x={2}
51
+ y={2}
52
+ {...{ alignmentBaseline: "central", textAnchor: "end", rotation: 0 }}
53
+ text={"TEST"}
54
+ animated={false}
55
+ d={d}
56
+ />
57
+ </svg>,
58
+ );
59
+
60
+ const path = unNullify(container.querySelector("path"), `path was null`);
61
+ expect(path).not.toBeNull();
62
+ expect(path.getAttribute("d")).toBe(d);
63
+ });
64
+
65
+ test("renders an animated label element that uses transform to position", () => {
66
+ render(
67
+ <svg>
68
+ <BaseLabel
69
+ x={2}
70
+ y={2}
71
+ {...{ alignmentBaseline: "central", textAnchor: "end", rotation: 0 }}
72
+ text={"TEST"}
73
+ animated={true}
74
+ />
75
+ </svg>,
76
+ );
77
+ const textEl = screen.getByText("TEST");
78
+ expect(textEl.tagName.toLowerCase()).toBe("text");
79
+ expect(textEl.getAttribute("transform")).toBe("translate(2,2) rotate(0)");
80
+ });
81
+ test("renders an animated label element an alignment line", () => {
82
+ const d =
83
+ "M 10,30 A 20,20 0,0,1 50,30 A 20,20 0,0,1 90,30Q 90,60 50,90 Q 10,60 10,30 z";
84
+ const { container } = render(
85
+ <svg>
86
+ <BaseLabel
87
+ x={2}
88
+ y={2}
89
+ {...{ alignmentBaseline: "central", textAnchor: "end", rotation: 0 }}
90
+ text={"TEST"}
91
+ animated={true}
92
+ d={d}
93
+ />
94
+ </svg>,
95
+ );
96
+
97
+ const path = unNullify(container.querySelector("path"), `path was null`);
98
+ expect(path).not.toBeNull();
99
+ expect(path.getAttribute("d")).toBe(d);
100
+ });
101
+ });
@@ -0,0 +1,67 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { render, screen } from "@testing-library/react";
3
+ import {
4
+ BaseRectangle,
5
+ CenteredRectangle,
6
+ } from "../../components/baubles/shapes";
7
+
8
+ describe("BaseRectangle", () => {
9
+ test("renders an SVG rect which is centered and has the right attributes", () => {
10
+ render(
11
+ <svg>
12
+ <CenteredRectangle
13
+ x={2}
14
+ y={2}
15
+ {...{ width: 2, height: 3, "data-testid": "rect" }}
16
+ animated={false}
17
+ />
18
+ </svg>,
19
+ );
20
+
21
+ const rect = screen.getByTestId("rect");
22
+ // const circle = container.querySelector("circle");
23
+
24
+ // expect(rect).toBeInTheDocument();
25
+ expect(rect.getAttribute("x")).toBe("1");
26
+ expect(rect.getAttribute("y")).toBe("0.5");
27
+ expect(rect.getAttribute("width")).toBe("2");
28
+ expect(rect.getAttribute("height")).toBe("3");
29
+ });
30
+
31
+ test("renders a rect which is not centered and has the right attributes", () => {
32
+ render(
33
+ <svg>
34
+ <BaseRectangle
35
+ x={2}
36
+ y={2}
37
+ {...{ width: 2, height: 3, "data-testid": "rect" }}
38
+ animated={false}
39
+ />
40
+ </svg>,
41
+ );
42
+
43
+ const rect = screen.getByTestId("rect");
44
+ expect(rect.getAttribute("x")).toBe("2");
45
+ expect(rect.getAttribute("y")).toBe("2");
46
+ expect(rect.getAttribute("width")).toBe("2");
47
+ expect(rect.getAttribute("height")).toBe("3");
48
+ });
49
+ test("renders an animated rect which is centered and has the right attributes", () => {
50
+ render(
51
+ <svg>
52
+ <BaseRectangle
53
+ x={2}
54
+ y={2}
55
+ {...{ width: 2, height: 3, "data-testid": "rect" }}
56
+ animated={true}
57
+ />
58
+ </svg>,
59
+ );
60
+
61
+ const rect = screen.getByTestId("rect");
62
+ expect(rect.getAttribute("x")).toBe("2");
63
+ expect(rect.getAttribute("y")).toBe("2");
64
+ expect(rect.getAttribute("width")).toBe("2");
65
+ expect(rect.getAttribute("height")).toBe("3");
66
+ });
67
+ });
@@ -0,0 +1 @@
1
+ export type WithTestId<T> = T & { "data-testid"?: string };
package/src/utils.ts ADDED
@@ -0,0 +1,57 @@
1
+ import {timeParse, timeFormat} from "d3-time-format";
2
+
3
+ //https://stackoverflow.com/questions/29400171/how-do-i-convert-a-decimal-year-value-into-a-date-in-javascript
4
+ /**
5
+ * Helper function to determine if the provided year is a leap year
6
+ * @param year
7
+ * @return {boolean}
8
+ */
9
+ export function leapYear(year:number):boolean {
10
+ return ((year % 4 === 0) && (year % 100 !== 0)) || (year % 400 === 0);
11
+ }
12
+
13
+ /**
14
+ * A function which converts a decimal float into a date object
15
+ * @param decimalDate
16
+ * @return {Date}
17
+ */
18
+ export function decimalToDate(decimal:number):Date{
19
+ const year = Math.trunc(decimal);
20
+ const totalNumberOfDays = leapYear(year)? 366:365;
21
+ const day = Math.round(((decimal-year)*totalNumberOfDays));
22
+ const date = timeParse("%Y-%j")(`${year}-${day}`)
23
+ // notNull(date,`Could not convert ${decimal} to date tried (year:${year} - day: ${day})`)
24
+ return unNullify(date,`Could not convert ${decimal} to date tried (year:${year} - day: ${day})`);
25
+ }
26
+
27
+ /**
28
+ * A function that converts a date into a decimal.
29
+ * @param date
30
+ * @return {number}
31
+ */
32
+ export function dateToDecimal(date:Date){
33
+ const year = parseInt(timeFormat("%Y")(date));
34
+ const day = parseInt(timeFormat("%j")(date));
35
+ const totalNumberOfDays = leapYear(year)? 366:365;
36
+ return year+day/totalNumberOfDays
37
+ }
38
+ export function panic(message: string): never { throw new Error(message); }
39
+
40
+
41
+ export function u<T>(x: T | undefined): NonNullable<T> {
42
+ if (x === undefined) {
43
+ throw new Error('internal bug! unhandled undefined');
44
+ }
45
+ return x as NonNullable<T>;
46
+ }
47
+
48
+
49
+ export function notNull<T>(x: T,message:string): asserts x is NonNullable<T> {
50
+ if (x === undefined) throw new Error(message);
51
+ }
52
+
53
+ export function unNullify<T>(x: T,message:string): NonNullable<T> {
54
+ if (x === null) throw new Error(message);
55
+ return x as NonNullable<T>
56
+ }
57
+
package/tsconfig.json ADDED
@@ -0,0 +1,12 @@
1
+ {
2
+ "extends": "@figtreejs/typescript-config/vite.json",
3
+ "compilerOptions": {
4
+ "allowSyntheticDefaultImports": true,
5
+ "noEmit": true,
6
+ "noUncheckedIndexedAccess": false,
7
+ "jsx": "react-jsx",
8
+ "types": ["vitest/globals"], // gives types for describe/test/expect
9
+ "lib": ["ES2020", "DOM", "DOM.Iterable"] // DOM types for `document`
10
+ },
11
+ "include": ["src/**/*.ts", "src/**/*.tsx", "*.js", "*.ts"] // /src files and config files
12
+ }
package/vite.config.ts ADDED
@@ -0,0 +1,34 @@
1
+ import dts from 'vite-plugin-dts'
2
+ import packageJson from "./package.json";
3
+ import { defineConfig } from 'vitest/config';
4
+
5
+ import react from '@vitejs/plugin-react';
6
+ // https://vitejs.dev/config/
7
+ export default defineConfig({
8
+ plugins: [react(),dts({ rollupTypes: true })],
9
+ build: {
10
+ lib: {
11
+ entry: 'src/index.ts',
12
+ name: packageJson.name, // used only for UMD/IIFE
13
+ formats: ['es', 'cjs'], // add 'umd' only if you need it
14
+ fileName: (format) => (format === 'es' ? 'index.mjs' : `index.${format}`)
15
+ },
16
+ rollupOptions: {
17
+ // mark peer deps external to avoid bundling react, etc.
18
+ external: ['react', 'react-dom','react/jsx-runtime'],
19
+ output: {
20
+ globals: {
21
+ react: 'React',
22
+ 'react-dom': 'ReactDOM' //react jsx runtime?
23
+ }
24
+ }
25
+ }
26
+ },
27
+ test: {
28
+ globals: true,
29
+ environment: 'jsdom', // makes `document` available at runtime
30
+ include: ['src/**/*.{test,spec}.{ts,tsx}'],
31
+ // setupFiles: ['./vitest.setup.ts'], // optional
32
+ },
33
+ });
34
+
@@ -0,0 +1,11 @@
1
+
2
+ import { defineConfig } from 'vitest/config';
3
+
4
+ export default defineConfig({
5
+ test: {
6
+ globals: true,
7
+ environment: 'jsdom', // makes `document` available at runtime
8
+ include: ['src/**/*.{test,spec}.{ts,tsx}'],
9
+ // setupFiles: ['./vitest.setup.ts'], // optional
10
+ },
11
+ });