Skip to content

Commit 9ec802b

Browse files
authored
refactor: add new auto arrangement method and improved demo changes (#5)
1 parent 1076ea5 commit 9ec802b

17 files changed

+590
-388
lines changed

projects/flow/README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Angular Flow
22

33
Angular Flow is a component that allows you to create a flow diagram using Angular.
4-
Live Demo [link](https://sheikalthaf.github.io/flow/)
4+
Live Demo [link](https://uiuniversal.github.io/flow/)
55

66
Stackblitz Demo [link](https://stackblitz.com/edit/ngu-flow)
77

projects/flow/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@ngu/flow",
3-
"version": "0.0.3",
3+
"version": "0.0.4",
44
"peerDependencies": {
55
"@angular/common": "^14.0.0",
66
"@angular/core": "^14.0.0"

projects/flow/src/lib/arrangements.spec.ts

+28-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Arrangements } from './arrangements';
1+
import { Arrangements, Arrangements2 } from './arrangements';
22
import { ChildInfo } from './flow-interface';
33

44
export const FLOW_LIST = [
@@ -38,3 +38,30 @@ describe('Arrangements', () => {
3838
expect(actual).toEqual(expected);
3939
});
4040
});
41+
42+
describe('Arrangements2', () => {
43+
let arrangements: Arrangements2;
44+
45+
it('should be created', () => {
46+
const childObj: ChildInfo[] = FLOW_LIST.map((x) => ({
47+
position: x,
48+
elRect: { width: 200, height: 200 } as any,
49+
}));
50+
51+
arrangements = new Arrangements2(childObj);
52+
arrangements.verticalPadding = 20;
53+
arrangements.groupPadding = 100;
54+
const expected = {
55+
'1': { x: 330, y: 0, id: '1', deps: [] },
56+
'2': { x: 110, y: 300, id: '2', deps: ['1'] },
57+
'3': { x: 0, y: 600, id: '3', deps: ['2'] },
58+
'4': { x: 220, y: 600, id: '4', deps: ['2'] },
59+
'5': { x: 550, y: 300, id: '5', deps: ['1'] },
60+
'6': { x: 440, y: 600, id: '6', deps: ['5'] },
61+
'7': { x: 660, y: 600, id: '7', deps: ['5'] },
62+
'8': { x: 660, y: 900, id: '8', deps: ['6', '7'] },
63+
};
64+
const actual = Object.fromEntries(arrangements.autoArrange());
65+
expect(actual).toEqual(expected);
66+
});
67+
});

projects/flow/src/lib/arrangements.ts

+112-5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { FlowOptions, ChildInfo } from './flow-interface';
1+
import { FlowOptions, ChildInfo, FlowDirection } from './flow-interface';
22

33
export class Arrangements {
44
constructor(
@@ -21,12 +21,12 @@ export class Arrangements {
2121

2222
for (const baseNode of baseNodes) {
2323
if (this.direction === 'horizontal') {
24-
this.positionDependents(baseNode, currentX, 0, newItems);
25-
currentX += baseNode.elRect.width + this.horizontalPadding;
24+
this.positionDependents(baseNode, 0, currentY, newItems);
25+
currentY += baseNode.elRect.height + this.verticalPadding;
2626
} else {
2727
// Vertical arrangement
28-
this.positionDependents(baseNode, 0, currentY, newItems);
29-
currentY += baseNode.elRect.height + this.horizontalPadding;
28+
this.positionDependents(baseNode, 0, currentX, newItems);
29+
currentX += baseNode.elRect.width + this.verticalPadding;
3030
}
3131
}
3232

@@ -116,3 +116,110 @@ export class Arrangements {
116116
return { consumedSpace, dep: dependents.length > 0 };
117117
}
118118
}
119+
120+
const ROOT_DATA = new Map<string, ArrangeNode>();
121+
const ROOT_DEPS = new Map<string, string[]>();
122+
const HORIZONTAL_PADDING = 100;
123+
const VERTICAL_PADDING = 20;
124+
125+
export class Arrangements2 {
126+
root: string[] = [];
127+
128+
constructor(
129+
private list: ChildInfo[],
130+
private direction: FlowDirection = 'vertical',
131+
public horizontalPadding = 100,
132+
public verticalPadding = 20,
133+
public groupPadding = 20
134+
) {
135+
ROOT_DATA.clear();
136+
ROOT_DEPS.clear();
137+
this.list.forEach((item) => {
138+
ROOT_DATA.set(
139+
item.position.id,
140+
new ArrangeNode(item.position, item.elRect)
141+
);
142+
item.position.deps.forEach((dep) => {
143+
let d = ROOT_DEPS.get(dep) || [];
144+
d.push(item.position.id);
145+
ROOT_DEPS.set(dep, d);
146+
});
147+
148+
if (item.position.deps.length === 0) {
149+
this.root.push(item.position.id);
150+
}
151+
});
152+
}
153+
154+
public autoArrange(): Map<string, FlowOptions> {
155+
this.root.forEach((id) => {
156+
const node = ROOT_DATA.get(id)!;
157+
node.arrange(0, 0, this.direction);
158+
});
159+
160+
const newItems = new Map<string, FlowOptions>();
161+
162+
for (const item of this.list) {
163+
newItems.set(item.position.id, item.position);
164+
}
165+
return newItems;
166+
}
167+
}
168+
169+
interface Coordinates {
170+
x: number;
171+
y: number;
172+
}
173+
174+
export class ArrangeNode {
175+
constructor(public position: FlowOptions, public elRect: DOMRect) {}
176+
177+
get deps() {
178+
return ROOT_DEPS.get(this.position.id) || [];
179+
}
180+
181+
// we need to recursively call this method to get all the dependents of the node
182+
// and then we need to position them
183+
arrange(x = 0, y = 0, direction: FlowDirection): Coordinates {
184+
const dependents = ROOT_DEPS.get(this.position.id) || [];
185+
let startX = x;
186+
let startY = y;
187+
let len = dependents.length;
188+
189+
if (len) {
190+
if (direction === 'horizontal') {
191+
startX += this.elRect.width + HORIZONTAL_PADDING;
192+
} else {
193+
startY += this.elRect.height + HORIZONTAL_PADDING;
194+
}
195+
let first, last: Coordinates;
196+
for (let i = 0; i < len; i++) {
197+
const dep = dependents[i];
198+
const dependent = ROOT_DATA.get(dep)!;
199+
const { x, y } = dependent.arrange(startX, startY, direction);
200+
// capture the first and last dependent
201+
if (i === 0) first = dependent.position;
202+
if (i === len - 1) last = dependent.position;
203+
204+
if (direction === 'horizontal') {
205+
startY = y + VERTICAL_PADDING;
206+
} else {
207+
startX = x + VERTICAL_PADDING;
208+
}
209+
}
210+
if (direction === 'horizontal') {
211+
startY -= VERTICAL_PADDING + this.elRect.height;
212+
y = first!.y + (last!.y - first!.y) / 2;
213+
} else {
214+
startX -= VERTICAL_PADDING + this.elRect.width;
215+
x = first!.x + (last!.x - first!.x) / 2;
216+
}
217+
}
218+
this.position.x = x;
219+
this.position.y = y;
220+
221+
return direction === 'horizontal'
222+
? { x: startX, y: startY + this.elRect.height }
223+
: { x: startX + this.elRect.width, y: startY };
224+
}
225+
}

projects/flow/src/lib/flow-child.component.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import {
1212
SimpleChanges,
1313
ChangeDetectionStrategy,
1414
} from '@angular/core';
15-
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
1615
import { Subject, Subscription } from 'rxjs';
1716
import { FlowService } from './flow.service';
1817
import { FlowOptions } from './flow-interface';
@@ -70,6 +69,7 @@ export class FlowChildComponent implements OnInit, OnChanges, OnDestroy {
7069

7170
private positionChange = new Subject<FlowOptions>();
7271
private mouseMoveSubscription: Subscription;
72+
private layoutSubscribe: Subscription;
7373

7474
constructor(
7575
public el: ElementRef<HTMLDivElement>,
@@ -89,7 +89,7 @@ export class FlowChildComponent implements OnInit, OnChanges, OnDestroy {
8989
});
9090
});
9191

92-
this.flow.layoutUpdated.pipe(takeUntilDestroyed()).subscribe((x) => {
92+
this.layoutSubscribe = this.flow.layoutUpdated.subscribe((x) => {
9393
this.position = this.flow.items.get(this.position.id) as FlowOptions;
9494
this.positionChange.next(this.position);
9595
});
@@ -171,6 +171,7 @@ export class FlowChildComponent implements OnInit, OnChanges, OnDestroy {
171171

172172
ngOnDestroy() {
173173
this.disableDragging();
174+
this.layoutSubscribe.unsubscribe();
174175
// remove the FlowOptions from the flow service
175176
// this.flow.delete(this.position);
176177
// console.log('ngOnDestroy', this.position.id);

projects/flow/src/lib/flow-interface.ts

+20
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,27 @@ export interface FlowOptions {
1212
deps: string[];
1313
}
1414

15+
export interface DotOptions extends FlowOptions {
16+
/**
17+
* The index of the dot
18+
* top = 0
19+
* right = 1
20+
* bottom = 2
21+
* left = 3
22+
*/
23+
dotIndex: number;
24+
}
25+
1526
export class FlowConfig {
1627
Arrows = true;
1728
ArrowSize = 20;
1829
}
30+
31+
export type FlowDirection = 'horizontal' | 'vertical';
32+
33+
export type ArrowPathFn = (
34+
start: DotOptions,
35+
end: DotOptions,
36+
arrowSize: number,
37+
strokeWidth: number
38+
) => string;

0 commit comments

Comments
 (0)