Skip to content

Commit c13a024

Browse files
committed
feat: new centered layout
Signed-off-by: Andres Correa Casablanca <castarco@coderspirit.xyz>
1 parent 6ace3a6 commit c13a024

File tree

6 files changed

+196
-23
lines changed

6 files changed

+196
-23
lines changed

src/BeautifulTree.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { edgesIterator, postOrderIterator } from './traversal'
22
import type { Tree } from './types'
33
import type { WrappedTreeWithLayout } from './layouts'
4-
import { computeLeftShiftLayout } from './layouts'
4+
export { computeLeftShiftLayout, computeCenter1Layout } from './layouts'
55

66
export interface BeautifulTreeProps {
77
readonly id: string
@@ -11,7 +11,7 @@ export interface BeautifulTreeProps {
1111
readonly sizeUnit?: '%' | 'em' | 'px' | 'rem'
1212
}
1313
readonly tree: Tree
14-
readonly computeLayout?: (
14+
readonly computeLayout: (
1515
tree: Readonly<Tree>,
1616
) => Readonly<WrappedTreeWithLayout>
1717
}
@@ -20,7 +20,7 @@ export function BeautifulTree({
2020
id,
2121
svgProps,
2222
tree,
23-
computeLayout = computeLeftShiftLayout,
23+
computeLayout,
2424
}: Readonly<BeautifulTreeProps>): JSX.Element {
2525
const { tree: treeWithLayout, maxX, maxY } = computeLayout(tree)
2626
const { width, height, sizeUnit = 'px' } = svgProps
@@ -60,8 +60,8 @@ export function BeautifulTree({
6060
)
6161
})}
6262
{[...postOrderIterator(treeWithLayout)].map((node, idx) => {
63-
const aX = node.meta.abstractPosition.x
64-
const aY = node.meta.abstractPosition.y
63+
const aX = node.meta.pos.x
64+
const aY = node.meta.pos.y
6565
return (
6666
<circle
6767
key={`${id}-node-${idx}`}

src/layouts.ts

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ const _computeLeftShiftLayout = (
2727
meta: {
2828
isRoot: depth === 0,
2929
isLeaf: tree.children === undefined || tree.children.length === 0,
30-
abstractPosition: { x, y: depth },
30+
pos: { x, y: depth },
3131
},
3232
} satisfies Readonly<TreeWithLayout>
3333
}
@@ -42,3 +42,95 @@ export const computeLeftShiftLayout = (
4242
maxY: counters.layers.length - 1,
4343
}
4444
}
45+
46+
type DeepWriteable<T> = { -readonly [P in keyof T]: DeepWriteable<T[P]> }
47+
48+
const _addMods = (
49+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
50+
tree: DeepWriteable<TreeWithLayout>,
51+
modsum = 0,
52+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
53+
tracer: { maxX: number },
54+
): void => {
55+
tree.meta.pos.x += modsum
56+
tracer.maxX = Math.max(tracer.maxX, tree.meta.pos.x)
57+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
58+
modsum += tree.meta.m! // We know it's defined because we control when it's called
59+
for (const child of tree.children ?? []) {
60+
_addMods(child.node, modsum, tracer)
61+
}
62+
}
63+
64+
const _computeCenter1Layout = (
65+
tree: Tree,
66+
depth = 0,
67+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
68+
offsets: number[],
69+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
70+
nexts: number[],
71+
): DeepWriteable<TreeWithLayout> => {
72+
const children = tree.children?.map((child) => ({
73+
edgeData: child.edgeData,
74+
node: _computeCenter1Layout(child.node, depth + 1, offsets, nexts),
75+
}))
76+
77+
let place: number
78+
let x: number
79+
80+
const numChildren = tree.children?.length ?? 0
81+
if (numChildren === 0) {
82+
place = nexts[depth] ?? 0
83+
x = place
84+
} else if (numChildren === 1) {
85+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
86+
place = children![0]!.node.meta.pos.x
87+
} else {
88+
place =
89+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
90+
(children![0]!.node.meta.pos.x +
91+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
92+
children![numChildren - 1]!.node.meta.pos.x) *
93+
0.5
94+
}
95+
96+
offsets[depth] = Math.max(offsets[depth] ?? 0, nexts[depth] ?? 0 - place)
97+
if (numChildren > 0) {
98+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
99+
x = place + offsets[depth]!
100+
}
101+
nexts[depth] = (nexts[depth] ?? 0) + 1
102+
103+
return {
104+
data: tree.data,
105+
children,
106+
meta: {
107+
isRoot: depth === 0,
108+
isLeaf: tree.children === undefined || tree.children.length === 0,
109+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
110+
pos: { x: x!, y: depth },
111+
m: offsets[depth],
112+
},
113+
} satisfies Readonly<TreeWithLayout> as DeepWriteable<TreeWithLayout>
114+
}
115+
116+
export const computeCenter1Layout = (
117+
tree: Readonly<Tree>,
118+
): Readonly<WrappedTreeWithLayout> => {
119+
const nexts: number[] = []
120+
const t = _computeCenter1Layout(tree, 0, [], nexts)
121+
122+
console.log('without mods')
123+
console.log(t)
124+
125+
const tracer = { maxX: 0 }
126+
_addMods(t, 0, tracer)
127+
128+
console.log('with mods')
129+
console.log(t)
130+
131+
return {
132+
tree: t,
133+
maxY: nexts.length - 1,
134+
maxX: tracer.maxX,
135+
}
136+
}

src/stories/BeautifulTree.stories.ts

Lines changed: 88 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1+
import {
2+
BeautifulTree,
3+
computeCenter1Layout,
4+
computeLeftShiftLayout,
5+
} from '../BeautifulTree'
16
import type { Meta, StoryObj } from '@storybook/react'
2-
import { BeautifulTree } from '../BeautifulTree'
37

48
const meta = {
59
title: 'BeautifulTree',
@@ -18,21 +22,54 @@ export default meta
1822

1923
type Story = StoryObj<typeof meta>
2024

21-
const testTree = {
25+
const smallTree = {
2226
data: { v: 42 },
2327
children: [
2428
{
25-
edgeData: {},
2629
node: {
2730
data: { v: 43 },
2831
children: [
2932
{
30-
edgeData: {},
3133
node: { data: { v: 45 } },
3234
},
3335
],
3436
},
3537
},
38+
{
39+
node: {
40+
data: { v: 44 },
41+
children: [
42+
{
43+
node: { data: { v: 46 } },
44+
},
45+
{
46+
node: { data: { v: 47 } },
47+
},
48+
],
49+
},
50+
},
51+
],
52+
}
53+
54+
const bigTree = {
55+
data: { v: 42 },
56+
children: [
57+
{
58+
node: {
59+
data: { v: 43 },
60+
children: [
61+
{
62+
node: {
63+
data: { v: 45 },
64+
children: [
65+
{ node: { data: { v: 48 } } },
66+
{ node: { data: { v: 49 } } },
67+
],
68+
},
69+
},
70+
],
71+
},
72+
},
3673
{
3774
edgeData: {},
3875
node: {
@@ -44,21 +81,64 @@ const testTree = {
4481
},
4582
{
4683
edgeData: {},
47-
node: { data: { v: 47 } },
84+
node: {
85+
data: { v: 47 },
86+
children: [
87+
{ node: { data: { v: 50 } } },
88+
{ node: { data: { v: 51 } } },
89+
],
90+
},
4891
},
4992
],
5093
},
5194
},
5295
],
5396
}
5497

55-
export const SimpleTree: Story = {
98+
export const LeftShifted_Tree: Story = {
99+
args: {
100+
id: 'leftshifted-beautiful-tree',
101+
svgProps: {
102+
width: 100,
103+
height: 100,
104+
},
105+
tree: smallTree,
106+
computeLayout: computeLeftShiftLayout,
107+
},
108+
}
109+
110+
export const LeftShifted_Big_Tree: Story = {
111+
args: {
112+
id: 'leftshifted-beautiful-tree',
113+
svgProps: {
114+
width: 100,
115+
height: 100,
116+
},
117+
tree: bigTree,
118+
computeLayout: computeLeftShiftLayout,
119+
},
120+
}
121+
122+
export const Centered1_Tree: Story = {
123+
args: {
124+
id: 'centered1-beautiful-tree',
125+
svgProps: {
126+
width: 100,
127+
height: 100,
128+
},
129+
tree: smallTree,
130+
computeLayout: computeCenter1Layout,
131+
},
132+
}
133+
134+
export const Centered1_Big_Tree: Story = {
56135
args: {
57-
id: 'simple-beautiful-tree',
136+
id: 'centered1-beautiful-tree',
58137
svgProps: {
59138
width: 100,
60139
height: 100,
61140
},
62-
tree: testTree,
141+
tree: bigTree,
142+
computeLayout: computeCenter1Layout,
63143
},
64144
}

src/tests/traversal.test.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -68,15 +68,15 @@ describe('edgesIterator', () => {
6868
meta: {
6969
isRoot: false,
7070
isLeaf: true,
71-
abstractPosition: { x: 0, y: 2 },
71+
pos: { x: 0, y: 2 },
7272
},
7373
},
7474
},
7575
],
7676
meta: {
7777
isRoot: false,
7878
isLeaf: false,
79-
abstractPosition: { x: 0, y: 1 },
79+
pos: { x: 0, y: 1 },
8080
},
8181
},
8282
},
@@ -92,7 +92,7 @@ describe('edgesIterator', () => {
9292
meta: {
9393
isRoot: false,
9494
isLeaf: true,
95-
abstractPosition: { x: 1, y: 2 },
95+
pos: { x: 1, y: 2 },
9696
},
9797
},
9898
},
@@ -103,23 +103,23 @@ describe('edgesIterator', () => {
103103
meta: {
104104
isRoot: false,
105105
isLeaf: true,
106-
abstractPosition: { x: 2, y: 2 },
106+
pos: { x: 2, y: 2 },
107107
},
108108
},
109109
},
110110
],
111111
meta: {
112112
isRoot: false,
113113
isLeaf: false,
114-
abstractPosition: { x: 1, y: 1 },
114+
pos: { x: 1, y: 1 },
115115
},
116116
},
117117
},
118118
],
119119
meta: {
120120
isRoot: true,
121121
isLeaf: false,
122-
abstractPosition: { x: 0, y: 0 },
122+
pos: { x: 0, y: 0 },
123123
},
124124
}
125125

src/traversal.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ export function* edgesIterator(
2020
): Generator<Readonly<Edge>, void> {
2121
for (const child of tree.children ?? []) {
2222
yield {
23-
start: tree.meta.abstractPosition,
24-
end: child.node.meta.abstractPosition,
23+
start: tree.meta.pos,
24+
end: child.node.meta.pos,
2525
edgeData: child.edgeData,
2626
}
2727
yield* edgesIterator(child.node)

src/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,11 @@ export type TreeWithLayout = Node & {
1818
readonly meta: {
1919
readonly isRoot: boolean
2020
readonly isLeaf: boolean
21-
readonly abstractPosition: {
21+
readonly pos: {
2222
readonly x: number
2323
readonly y: number
2424
}
25+
readonly m?: number | undefined
2526
}
2627
}
2728

0 commit comments

Comments
 (0)