Skip to content

Commit

Permalink
feat: Generates the graph with the given values
Browse files Browse the repository at this point in the history
  • Loading branch information
FrOZEn-FurY committed Aug 27, 2024
1 parent 8dc182f commit 7fb541f
Show file tree
Hide file tree
Showing 10 changed files with 982 additions and 25 deletions.
644 changes: 644 additions & 0 deletions project/package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions project/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
"@angular/platform-server": "^18.1.0",
"@angular/router": "^18.1.0",
"@angular/ssr": "^18.1.2",
"@types/d3": "^7.4.3",
"d3": "^7.9.0",
"express": "^4.18.2",
"jalaali-js": "^1.2.7",
"rxjs": "~7.8.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export class ManageUsersComponent {

ngOnInit(): void {
const token = this.getToken();
this.http.get<User[]>(API_BASE_URL + 'Identity/GetUsers', {headers: {'Authorization': "bearer " + token}})
this.http.get<User[]>(API_BASE_URL + 'identity', {headers: {'Authorization': "bearer " + token}})
.subscribe((response) => {
this.users = response;
this.finalUsers = response;
Expand Down Expand Up @@ -83,7 +83,7 @@ export class ManageUsersComponent {
role: this.formGroup.value.role,
}
const token = this.getToken();
this.http.post<User>(API_BASE_URL + 'Identity/Signup', data, {headers: {'Authorization': "Bearer " + token}})
this.http.post<User>(API_BASE_URL + 'identity/signup', data, {headers: {'Authorization': "Bearer " + token}})
.subscribe((res) => {
this.finalUsers?.push(res);
this.handleClose();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@
}

<button data-trigger-button class="show-button" (click)="showData()">نشان دادن داده ها به صورت جدول</button><hr />
<div class="form-search-user">
<label for="search-user">شماره حساب کاربر:</label>
<input id="search-user" type="search" placeholder="جستجو و نمایش گرافی داده های تراکنش یک کاربر" />
</div>
<div #dataElement class="table-container" appBlurClick (blurClick)="handleClose()">
<ng-icon name="heroXMark" class="xicon" (click)="handleClose()"></ng-icon>
<table class="table">
Expand Down Expand Up @@ -40,6 +44,11 @@ <h4>هیچ داده ای برای نمایش یافت نشد.</h4>
</tbody>
</table>
</div>
<div id="graph-container" #graphElement></div>
</section>

<div id="graph-container"></div>
<div #contextElement class="context-menu-container" appBlurClick (blurClick)="handleCloseContext()">
<ul>
<li>نمایش کاربر</li>
<li>افزایش تراکنش ها</li>
</ul>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,48 @@
@apply absolute w-[4rem] h-[4rem] top-0 right-0;
color: $bg-color;
}
.form-search-user {
@apply text-[2rem] self-center border rounded-2xl transition-all duration-300 ease-in-out p-4 flex flex-row
gap-4 items-center;
inline-size: 70rem;

input {
@apply flex-grow p-4 rounded-2xl;
}
}
#graph-container {
@apply self-stretch flex-grow m-8 rounded-xl;
background-color: $secondary-color;
}
}
.context-menu-container {
position: absolute;

display: none;

padding: 1rem;

flex-direction: column;
justify-content: center;
align-items: center;

background-color: $primary-color;
color: $bg-color;

font-size: 1.6rem;

ul {
list-style: none;

padding: 0.1rem;

li {
padding: 0.5rem;
user-select: none;

&:hover {
cursor: pointer;
}
}
}
}
237 changes: 220 additions & 17 deletions project/src/app/components/dashboard/show-data/show-data.component.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {Component, ElementRef, ViewChild} from '@angular/core';
import {Component, ElementRef, EventEmitter, HostListener, Output, ViewChild} from '@angular/core';
import {UserService} from "../../../services/user/user.service";
import User from "../../../interfaces/user";
import {FormsModule} from "@angular/forms";
Expand All @@ -9,6 +9,8 @@ import {PersianDatePipe} from "./pipes/persian-date.pipe";
import {heroXMark} from "@ng-icons/heroicons/outline";
import {NgIconComponent, provideIcons} from "@ng-icons/core";
import {BlurClickDirective} from "../../../directives/blur-click.directive";
import * as d3 from 'd3';
import {FetchDataService} from "../../../services/fetchData/fetch-data.service";

interface Transaction {
TransactionId: number,
Expand All @@ -19,6 +21,24 @@ interface Transaction {
type: string
}

interface Node {
x: number;
y: number;
vx: number;
vy: number;
fx?: number | null;
fy?: number | null;
label: string | number;
}

interface Link {
source: Node;
target: Node;
date: string;
amount: string;
type: string;
}

@Component({
selector: 'app-show-data',
standalone: true,
Expand All @@ -36,14 +56,19 @@ interface Transaction {
export class ShowDataComponent {
user!: User | undefined;
data: Transaction[] | undefined = undefined;
dataGot = false;
nodes: Node[] = [];
links: Link[] = [];

@Output() dataGotEvent = new EventEmitter();

@ViewChild('labelElement') labelElement!: ElementRef<HTMLLabelElement>;
@ViewChild('inputElement') inputElement!: ElementRef<HTMLInputElement>;
@ViewChild('selectElement') selectElement!: ElementRef<HTMLSelectElement>;
@ViewChild('dataElement') dataElement!: ElementRef<HTMLDivElement>;
@ViewChild('graphElement') graphElement!: ElementRef<HTMLDivElement>;
@ViewChild('contextElement') contextElement!: ElementRef<HTMLDivElement>;

constructor(private userService: UserService, private http: HttpClient) {
constructor(private userService: UserService, private http: HttpClient, private fetchDataService: FetchDataService) {
this.user = this.userService.getUser();
}

Expand All @@ -64,30 +89,18 @@ export class ShowDataComponent {
const file = this.inputElement.nativeElement.files[0];
formData.append('file', file);
if (this.selectElement.nativeElement.value === "transaction") {
this.http.post(API_BASE_URL + 'Transaction/ImportTransactions', formData, {headers: {"Authorization": "Bearer " + token}}).subscribe((response) => {
this.http.post(API_BASE_URL + 'transactions/upload', formData, {headers: {"Authorization": "Bearer " + token}}).subscribe((response) => {
console.log(response);
})
} else if (this.selectElement.nativeElement.value === "account") {
this.http.post(API_BASE_URL + 'Account/ImportAccounts', formData, {headers: {"Authorization": "Bearer " + token}}).subscribe((response) => {
this.http.post(API_BASE_URL + 'accounts/upload', formData, {headers: {"Authorization": "Bearer " + token}}).subscribe((response) => {
console.log(response);
})
}
}
}

updateData(): void {
this.dataGot = false;
const token: string | null = this.getToken();
this.http.get<Transaction[]>(API_BASE_URL + 'Transaction/GetAllTransactions', {headers: {"Authorization": "Bearer " + token}}).subscribe((response) => {
this.data = response;
this.dataGot = true;
})
}

showData(): void {
if (this.data === undefined) {
this.updateData();
}
this.dataElement.nativeElement.style.display = 'flex';
}

Expand All @@ -103,4 +116,194 @@ export class ShowDataComponent {

return token;
}

async ngOnInit() {
const response = await this.fetchDataService.fetchData();
this.data = response;
for (const trans of response) {
if (!this.nodes.find(node => node.label === trans.sourceAccountId)) {
this.nodes.push({
x: this.nodes[this.nodes.length - 1] ? this.nodes[this.nodes.length - 1].x + 1 : 1,
y: this.nodes[this.nodes.length - 1] ? this.nodes[this.nodes.length - 1].y + 1 : 1,
vx: 1,
vy: 1,
label: trans.sourceAccountId,
});
}
if (!this.nodes.find(node => node.label === trans.destinationAccountId)) {
this.nodes.push({
x: this.nodes[this.nodes.length - 1] ? this.nodes[this.nodes.length - 1].x + 1 : 1,
y: this.nodes[this.nodes.length - 1] ? this.nodes[this.nodes.length - 1].y + 1 : 1,
vx: 1,
vy: 1,
label: trans.destinationAccountId,
});
}
this.links.push({
source: this.nodes.find(node => node.label === trans.sourceAccountId)!,
target: this.nodes.find(node => node.label === trans.destinationAccountId)!,
date: (new PersianDatePipe()).transform(trans.date),
type: trans.type,
amount: (new RialPipePipe()).transform(trans.amount),
});
}
this.dataGotEvent.emit();
}

@HostListener('dataGotEvent')
handleGraph(): void {
const element = d3.select(this.graphElement.nativeElement)
.append('svg')
.attr('width', this.graphElement.nativeElement.clientWidth)
.attr('height', this.graphElement.nativeElement.clientHeight);


const svgGroup = element.append('g');

const zoom = d3.zoom<SVGSVGElement, unknown>()
.scaleExtent([0.5, 4])
.on('zoom', (event) => {
svgGroup.attr('transform', event.transform);
});

element.call(zoom);

const simulation = d3.forceSimulation(this.nodes)
.force("link", d3.forceLink(this.links));

const link = svgGroup.append('g')
.attr('class', 'links')
.selectAll('line')
.data(this.links)
.enter()
.append('line')
.attr('stroke-width', 2)
.attr('stroke', '#FDFDFD');


const linkLabelsAmount = svgGroup.append('g')
.attr('class', 'link-labels')
.selectAll('text')
.data(this.links)
.enter()
.append('text')
.attr('text-anchor', 'middle')
.attr('fill', '#172535')
.attr('style', 'user-select: none;font-weight:bold;font-size:1.5rem;')
.text((d: Link) => d.amount ? d.amount : "");

const linkLabelsType = svgGroup.append('g')
.attr('class', 'link-labels')
.selectAll('text')
.data(this.links)
.enter()
.append('text')
.attr('text-anchor', 'middle')
.attr('fill', '#172535')
.attr('style', 'user-select: none;font-weight:bold;font-size:1.5rem;')
.text((d: Link) => d.type ? d.type : "");

const linkLabelsDate = svgGroup.append('g')
.attr('class', 'link-labels')
.selectAll('text')
.data(this.links)
.enter()
.append('text')
.attr('text-anchor', 'middle')
.attr('fill', '#172535')
.attr('style', 'user-select: none;font-weight:bold;font-size:1.5rem;')
.text((d: Link) => d.date ? d.date : "");

const node = svgGroup.append('g')
.attr('class', 'nodes')
.selectAll('circle')
.data(this.nodes)
.enter()
.append('circle')
.attr('r', 10)
.attr('fill', '#002B5B')
.call(d3.drag<SVGCircleElement, Node>()
.on('start', dragStarted)
.on('drag', dragged)
.on('end', dragEnded)
);

node.on('contextmenu', (event: MouseEvent, d: Node) => {
event.preventDefault();

this.contextElement.nativeElement.style.display = 'flex';
this.contextElement.nativeElement.style.top = event.clientY + 'px';
this.contextElement.nativeElement.style.left = event.clientX + 'px';
});

const nodeLabels = svgGroup.append('g')
.attr('class', 'node-labels')
.selectAll('text')
.data(this.nodes)
.enter()
.append('text')
.attr('text-anchor', 'middle')
.attr('dy', -10) // Position above the node
.attr('fill', '#172535')
.attr('style', 'user-select: none;font-weight:bold;font-size:1.5rem;')
.text((d: Node) => d.label ? d.label : "");

function ticked() {
link
.attr('x1', d => d.source.x)
.attr('y1', d => d.source.y)
.attr('x2', d => d.target.x)
.attr('y2', d => d.target.y);

node
.attr('cx', d => d.x)
.attr('cy', d => d.y);

// Update positions of node labels
nodeLabels
.attr('x', d => d.x)
.attr('y', d => d.y);

// Update positions of link labels
linkLabelsAmount
.attr('x', d => ((d.source as Node).x + (d.target as Node).x) / 2)
.attr('y', d => ((d.source as Node).y + (d.target as Node).y) / 2);

linkLabelsDate
.attr('x', d => ((d.source as Node).x + (d.target as Node).x) / 2)
.attr('y', d => ((d.source as Node).y + (d.target as Node).y) / 2 + 20);

linkLabelsType
.attr('x', d => ((d.source as Node).x + (d.target as Node).x) / 2)
.attr('y', d => ((d.source as Node).y + (d.target as Node).y) / 2 + 40);
}

simulation.on('tick', ticked);

simulation
.force('link', d3.forceLink(this.links).id((d, i) => i).distance(250))
.force('charge', d3.forceManyBody().strength(-350))
.force('center', d3.forceCenter(this.graphElement.nativeElement.clientWidth / 2, this.graphElement.nativeElement.clientHeight / 2));

function dragStarted(event: d3.D3DragEvent<SVGCircleElement, Node, Node>, d: Node) {
if (!event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}

function dragged(event: d3.D3DragEvent<SVGCircleElement, Node, Node>, d: Node) {
d.fx = event.x;
d.fy = event.y;
}

function dragEnded(event: d3.D3DragEvent<SVGCircleElement, Node, Node>, d: Node) {
if (!event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
}

handleCloseContext() {
this.contextElement.nativeElement.style.display = 'none';
}
}
Loading

0 comments on commit 7fb541f

Please sign in to comment.