Skip to content

Commit ef16697

Browse files
committed
feat: draw edges
Signed-off-by: Andres Correa Casablanca <castarco@coderspirit.xyz>
1 parent ec81061 commit ef16697

File tree

4 files changed

+140
-14
lines changed

4 files changed

+140
-14
lines changed

src/BeautifulTree.tsx

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1+
import { edgesIterator, postOrderIterator } from './traversal'
12
import type { Tree } from './types'
23
import { computeLeftShiftLayout } from './layouts'
3-
import { postOrderIterator } from './traversal'
44

55
export interface BeautifulTreeProps {
66
readonly id: string
@@ -18,10 +18,11 @@ export function BeautifulTree({
1818
tree,
1919
}: Readonly<BeautifulTreeProps>): JSX.Element {
2020
const { tree: treeWithLayout, maxX, maxY } = computeLeftShiftLayout(tree)
21-
const orderedNodes = [...postOrderIterator(treeWithLayout)]
22-
2321
const { width, height, sizeUnit = 'px' } = svgProps
2422

23+
const xDivisor = maxX + 2
24+
const yDivisor = maxY + 2
25+
2526
return (
2627
<svg
2728
xmlns="http://www.w3.org/2000/svg"
@@ -33,15 +34,29 @@ export function BeautifulTree({
3334
}}
3435
className={'beautiful-tree-react'}
3536
>
36-
{orderedNodes.map((node, idx) => {
37+
{/* TODO: introduce edge "styles" (straight, cornered, curved..., plus CSS styles) */}
38+
{[...edgesIterator(treeWithLayout)].map((edge, idx) => {
39+
return (
40+
<line
41+
key={`${id}-edge-${idx}`}
42+
x1={((edge.start.x + 1) * width) / xDivisor}
43+
y1={((edge.start.y + 1) * height) / yDivisor}
44+
x2={((edge.end.x + 1) * width) / xDivisor}
45+
y2={((edge.end.y + 1) * height) / yDivisor}
46+
stroke="black"
47+
/>
48+
)
49+
})}
50+
51+
{[...postOrderIterator(treeWithLayout)].map((node, idx) => {
3752
const aX = node.meta.abstractPosition.x
3853
const aY = node.meta.abstractPosition.y
3954
return (
4055
<circle
4156
key={`${id}-node-${idx}`}
4257
className={'beautiful-tree-node'}
43-
cx={((aX + 1) * width) / (maxX + 2)}
44-
cy={((aY + 1) * height) / (maxY + 2)}
58+
cx={((aX + 1) * width) / xDivisor}
59+
cy={((aY + 1) * height) / yDivisor}
4560
stroke="black"
4661
fill="white"
4762
r="5"

src/tests/traversal.test.ts

Lines changed: 87 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { describe, expect, it } from 'vitest'
2-
import { postOrderIterator } from '../traversal'
2+
import { edgesIterator, postOrderIterator } from '../traversal'
3+
import type { TreeWithLayout } from '../types'
34

45
describe('postOrderIterator', () => {
56
const testTree = {
@@ -38,8 +39,6 @@ describe('postOrderIterator', () => {
3839

3940
it('traverses tree in post-order', () => {
4041
const result = postOrderIterator(testTree)
41-
42-
console.log('result:')
4342
const expandedResult = [...result]
4443

4544
expect(expandedResult).toEqual([
@@ -52,3 +51,88 @@ describe('postOrderIterator', () => {
5251
])
5352
})
5453
})
54+
55+
describe('edgesIterator', () => {
56+
const testTree: TreeWithLayout = {
57+
data: { v: 42 },
58+
children: [
59+
{
60+
edgeData: { e: 100 },
61+
node: {
62+
data: { v: 43 },
63+
children: [
64+
{
65+
edgeData: { e: 102 },
66+
node: {
67+
data: { v: 45 },
68+
meta: {
69+
isRoot: false,
70+
isLeaf: true,
71+
abstractPosition: { x: 0, y: 2 },
72+
},
73+
},
74+
},
75+
],
76+
meta: {
77+
isRoot: false,
78+
isLeaf: false,
79+
abstractPosition: { x: 0, y: 1 },
80+
},
81+
},
82+
},
83+
{
84+
edgeData: { e: 101 },
85+
node: {
86+
data: { v: 44 },
87+
children: [
88+
{
89+
edgeData: { e: 103 },
90+
node: {
91+
data: { v: 46 },
92+
meta: {
93+
isRoot: false,
94+
isLeaf: true,
95+
abstractPosition: { x: 1, y: 2 },
96+
},
97+
},
98+
},
99+
{
100+
edgeData: { e: 104 },
101+
node: {
102+
data: { v: 47 },
103+
meta: {
104+
isRoot: false,
105+
isLeaf: true,
106+
abstractPosition: { x: 2, y: 2 },
107+
},
108+
},
109+
},
110+
],
111+
meta: {
112+
isRoot: false,
113+
isLeaf: false,
114+
abstractPosition: { x: 1, y: 1 },
115+
},
116+
},
117+
},
118+
],
119+
meta: {
120+
isRoot: true,
121+
isLeaf: false,
122+
abstractPosition: { x: 0, y: 0 },
123+
},
124+
}
125+
126+
it('traverses edges in pre-order', () => {
127+
const result = edgesIterator(testTree)
128+
const expandedResult = [...result]
129+
130+
expect(expandedResult).toEqual([
131+
{ start: { x: 0, y: 0 }, end: { x: 0, y: 1 }, edgeData: { e: 100 } },
132+
{ start: { x: 0, y: 1 }, end: { x: 0, y: 2 }, edgeData: { e: 102 } },
133+
{ start: { x: 0, y: 0 }, end: { x: 1, y: 1 }, edgeData: { e: 101 } },
134+
{ start: { x: 1, y: 1 }, end: { x: 1, y: 2 }, edgeData: { e: 103 } },
135+
{ start: { x: 1, y: 1 }, end: { x: 2, y: 2 }, edgeData: { e: 104 } },
136+
])
137+
})
138+
})

src/traversal.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,29 @@
1-
import type { Tree, TreeWithLayout } from './types'
1+
import type { Edge, Tree, TreeWithLayout } from './types'
22

33
export function* postOrderIterator<T extends Tree | TreeWithLayout = Tree>(
4-
v: Readonly<T>,
4+
tree: Readonly<T>,
55
): Generator<Omit<Readonly<T>, 'children'>, void> {
6-
for (const child of v.children ?? []) {
6+
for (const child of tree.children ?? []) {
77
yield* postOrderIterator<T>(child.node as T)
88
}
99

1010
// We'll discard children, but we don't directly access v.data because we
1111
// might also want to access v.layout in case it exists.
1212

1313
// eslint-disable-next-line @typescript-eslint/no-unused-vars
14-
const { children, ...vv } = v
14+
const { children, ...vv } = tree
1515
yield vv
1616
}
17+
18+
export function* edgesIterator(
19+
tree: Readonly<TreeWithLayout>,
20+
): Generator<Readonly<Edge>, void> {
21+
for (const child of tree.children ?? []) {
22+
yield {
23+
start: tree.meta.abstractPosition,
24+
end: child.node.meta.abstractPosition,
25+
edgeData: child.edgeData,
26+
}
27+
yield* edgesIterator(child.node)
28+
}
29+
}

src/types.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ export type Tree = Node & {
1212
}
1313

1414
export type TreeWithLayout = Node & {
15-
readonly children?: Required<TreeChild<TreeWithLayout>>[] | undefined
15+
readonly children?:
16+
| Readonly<Required<TreeChild<TreeWithLayout>>[]>
17+
| undefined
1618
readonly meta: {
1719
readonly isRoot: boolean
1820
readonly isLeaf: boolean
@@ -22,3 +24,15 @@ export type TreeWithLayout = Node & {
2224
}
2325
}
2426
}
27+
28+
export interface Edge {
29+
readonly start: {
30+
readonly x: number
31+
readonly y: number
32+
}
33+
readonly end: {
34+
readonly x: number
35+
readonly y: number
36+
}
37+
readonly edgeData?: Readonly<Record<string, unknown>> | undefined
38+
}

0 commit comments

Comments
 (0)