Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: add new auto arrangement method and improved demo changes #5

Merged
merged 1 commit into from
Jan 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion projects/flow/README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Angular Flow

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

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

Expand Down
2 changes: 1 addition & 1 deletion projects/flow/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ngu/flow",
"version": "0.0.3",
"version": "0.0.4",
"peerDependencies": {
"@angular/common": "^14.0.0",
"@angular/core": "^14.0.0"
Expand Down
29 changes: 28 additions & 1 deletion projects/flow/src/lib/arrangements.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Arrangements } from './arrangements';
import { Arrangements, Arrangements2 } from './arrangements';
import { ChildInfo } from './flow-interface';

export const FLOW_LIST = [
Expand Down Expand Up @@ -38,3 +38,30 @@ describe('Arrangements', () => {
expect(actual).toEqual(expected);
});
});

describe('Arrangements2', () => {
let arrangements: Arrangements2;

it('should be created', () => {
const childObj: ChildInfo[] = FLOW_LIST.map((x) => ({
position: x,
elRect: { width: 200, height: 200 } as any,
}));

arrangements = new Arrangements2(childObj);
arrangements.verticalPadding = 20;
arrangements.groupPadding = 100;
const expected = {
'1': { x: 330, y: 0, id: '1', deps: [] },
'2': { x: 110, y: 300, id: '2', deps: ['1'] },
'3': { x: 0, y: 600, id: '3', deps: ['2'] },
'4': { x: 220, y: 600, id: '4', deps: ['2'] },
'5': { x: 550, y: 300, id: '5', deps: ['1'] },
'6': { x: 440, y: 600, id: '6', deps: ['5'] },
'7': { x: 660, y: 600, id: '7', deps: ['5'] },
'8': { x: 660, y: 900, id: '8', deps: ['6', '7'] },
};
const actual = Object.fromEntries(arrangements.autoArrange());
expect(actual).toEqual(expected);
});
});
117 changes: 112 additions & 5 deletions projects/flow/src/lib/arrangements.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FlowOptions, ChildInfo } from './flow-interface';
import { FlowOptions, ChildInfo, FlowDirection } from './flow-interface';

export class Arrangements {
constructor(
Expand All @@ -21,12 +21,12 @@ export class Arrangements {

for (const baseNode of baseNodes) {
if (this.direction === 'horizontal') {
this.positionDependents(baseNode, currentX, 0, newItems);
currentX += baseNode.elRect.width + this.horizontalPadding;
this.positionDependents(baseNode, 0, currentY, newItems);
currentY += baseNode.elRect.height + this.verticalPadding;
} else {
// Vertical arrangement
this.positionDependents(baseNode, 0, currentY, newItems);
currentY += baseNode.elRect.height + this.horizontalPadding;
this.positionDependents(baseNode, 0, currentX, newItems);
currentX += baseNode.elRect.width + this.verticalPadding;
}
}

Expand Down Expand Up @@ -116,3 +116,110 @@ export class Arrangements {
return { consumedSpace, dep: dependents.length > 0 };
}
}

const ROOT_DATA = new Map<string, ArrangeNode>();
const ROOT_DEPS = new Map<string, string[]>();
const HORIZONTAL_PADDING = 100;
const VERTICAL_PADDING = 20;

export class Arrangements2 {
root: string[] = [];

constructor(
private list: ChildInfo[],
private direction: FlowDirection = 'vertical',
public horizontalPadding = 100,
public verticalPadding = 20,
public groupPadding = 20
) {
ROOT_DATA.clear();
ROOT_DEPS.clear();
this.list.forEach((item) => {
ROOT_DATA.set(
item.position.id,
new ArrangeNode(item.position, item.elRect)
);
item.position.deps.forEach((dep) => {
let d = ROOT_DEPS.get(dep) || [];
d.push(item.position.id);
ROOT_DEPS.set(dep, d);
});

if (item.position.deps.length === 0) {
this.root.push(item.position.id);
}
});
}

public autoArrange(): Map<string, FlowOptions> {
this.root.forEach((id) => {
const node = ROOT_DATA.get(id)!;
node.arrange(0, 0, this.direction);
});

const newItems = new Map<string, FlowOptions>();

for (const item of this.list) {
newItems.set(item.position.id, item.position);
}
return newItems;
}
}

interface Coordinates {
x: number;
y: number;
}

export class ArrangeNode {
constructor(public position: FlowOptions, public elRect: DOMRect) {}

get deps() {
return ROOT_DEPS.get(this.position.id) || [];
}

// we need to recursively call this method to get all the dependents of the node
// and then we need to position them
arrange(x = 0, y = 0, direction: FlowDirection): Coordinates {
const dependents = ROOT_DEPS.get(this.position.id) || [];
let startX = x;
let startY = y;
let len = dependents.length;

if (len) {
if (direction === 'horizontal') {
startX += this.elRect.width + HORIZONTAL_PADDING;
} else {
startY += this.elRect.height + HORIZONTAL_PADDING;
}
let first, last: Coordinates;
for (let i = 0; i < len; i++) {
const dep = dependents[i];
const dependent = ROOT_DATA.get(dep)!;
const { x, y } = dependent.arrange(startX, startY, direction);
// capture the first and last dependent
if (i === 0) first = dependent.position;
if (i === len - 1) last = dependent.position;

if (direction === 'horizontal') {
startY = y + VERTICAL_PADDING;
} else {
startX = x + VERTICAL_PADDING;
}
}
if (direction === 'horizontal') {
startY -= VERTICAL_PADDING + this.elRect.height;
y = first!.y + (last!.y - first!.y) / 2;
} else {
startX -= VERTICAL_PADDING + this.elRect.width;
x = first!.x + (last!.x - first!.x) / 2;
}
}
this.position.x = x;
this.position.y = y;

return direction === 'horizontal'
? { x: startX, y: startY + this.elRect.height }
: { x: startX + this.elRect.width, y: startY };
}
}
5 changes: 3 additions & 2 deletions projects/flow/src/lib/flow-child.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import {
SimpleChanges,
ChangeDetectionStrategy,
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { Subject, Subscription } from 'rxjs';
import { FlowService } from './flow.service';
import { FlowOptions } from './flow-interface';
Expand Down Expand Up @@ -70,6 +69,7 @@ export class FlowChildComponent implements OnInit, OnChanges, OnDestroy {

private positionChange = new Subject<FlowOptions>();
private mouseMoveSubscription: Subscription;
private layoutSubscribe: Subscription;

constructor(
public el: ElementRef<HTMLDivElement>,
Expand All @@ -89,7 +89,7 @@ export class FlowChildComponent implements OnInit, OnChanges, OnDestroy {
});
});

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

ngOnDestroy() {
this.disableDragging();
this.layoutSubscribe.unsubscribe();
// remove the FlowOptions from the flow service
// this.flow.delete(this.position);
// console.log('ngOnDestroy', this.position.id);
Expand Down
20 changes: 20 additions & 0 deletions projects/flow/src/lib/flow-interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,27 @@ export interface FlowOptions {
deps: string[];
}

export interface DotOptions extends FlowOptions {
/**
* The index of the dot
* top = 0
* right = 1
* bottom = 2
* left = 3
*/
dotIndex: number;
}

export class FlowConfig {
Arrows = true;
ArrowSize = 20;
}

export type FlowDirection = 'horizontal' | 'vertical';

export type ArrowPathFn = (
start: DotOptions,
end: DotOptions,
arrowSize: number,
strokeWidth: number
) => string;
Loading
Loading