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

I want to add D3 Force on my CustomNodeComponent, occurring signal issue #94

Open
mayurpatil5953 opened this issue Oct 14, 2024 · 1 comment

Comments

@mayurpatil5953
Copy link

data-lineage-ngx-vflow.component.html :
`<!--

{{ dataLineageTitle }}
close
-->

{{ dataLineageTitle }}

close
@if (dataLineageForType && dataLineageForValue) {
{{ dataLineageForType }} : {{ dataLineageForValue }}
}@else { No Data Found }
{{ ctx.node.data.groupName }} ({{ ctx.node.data.nodeCount }})
You can zoom in or zoom out on the data lineage graph by pinching/scrolling.
`

`import {
Component,
NO_ERRORS_SCHEMA,
// ChangeDetectionStrategy,
OnInit,
Inject,
ViewChild,
signal,
ChangeDetectorRef
} from '@angular/core';
import { Node, Edge, VflowModule, ComponentNodeEvent, VflowComponent } from 'ngx-vflow';
import { CustomNodeForLineageComponent } from './custom-node/custom-node.component';
import {DataLineageService} from '../../../services/data-lineage.service';
import * as d3 from 'd3-force';
import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';

type Point = {
x: number;
y: number;
};

type WritableSignal = {
value: T;
// other properties and methods
};

@component({
selector: 'app-data-lineage-ngx-vflow',
// changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [VflowModule, MatDialogModule ],
templateUrl: './data-lineage-ngx-vflow.component.html',
styleUrl: './data-lineage-ngx-vflow.component.scss',
schemas: [NO_ERRORS_SCHEMA],
})
export class DataLineageNgxVflowComponent implements OnInit {
[x: string]: any;
@ViewChild(VflowComponent) vflow!: VflowComponent;
incomingLineageType: any;
incomingLineageData: any;

constructor(
@Inject(MAT_DIALOG_DATA) public data: any,
@Inject(DataLineageService) private dataLineageService: DataLineageService,
private cdr: ChangeDetectorRef
){}

public nodes: Node[] = [];
public edges: Edge[] = [];
public handleTypeForGroupUse: any;
dataLineageTitle : any;
dataLineageForType : any;
dataLineageForValue : any;
sampleApiResponse : any;

// apiResponse = {
// "groups": [
// {
// "groupName": "Domain",
// "groupId": "group_g001",
// "nodes": [
// {
// "id": "domain_1",
// "name": "Insurance",
// "type": "Domain"
// }
// ]
// },
// {
// "groupName": "Subdomain",
// "groupId": "group_g002",
// "nodes": [
// {
// "id": "subdomain_2",
// "name": "Life Insurance",
// "type": "SubDomain"
// },
// {
// "id": "subdomain_3",
// "name": "Insurance Term",
// "type": "SubDomain"
// }
// ]
// },
// {
// "groupName": "Consumption Aligned Data Product",
// "groupId": "group_g003",
// "nodes": [
// {
// "id": "dataProduct_21",
// "name": "Policy Holder",
// "type": "DataProduct"
// }
// ]
// },
// {
// "groupName": "Intermediate Data Product",
// "groupId": "group_g004",
// "nodes": [
// {
// "id": "dataProduct_22",
// "name": "Customer Satisfaction",
// "type": "DataProduct"
// }
// ]
// },
// {
// "groupName": "Source Aligned Data Product",
// "groupId": "group_g005",
// "nodes": [
// {
// "id": "dataProduct_23",
// "name": "Fraud Detection",
// "type": "DataProduct"
// },
// {
// "id": "dataProduct_24",
// "name": "Risk Assessment",
// "type": "DataProduct"
// }
// ]
// },
// {
// "groupName": "Consumption Aligned Data Product",
// "groupId": "group_g006",
// "nodes": [
// {
// "id": "dataProduct_25",
// "name": "Telematics",
// "type": "DataProduct"
// },
// {
// "id": "dataProduct_26",
// "name": "Life Expectancy",
// "type": "DataProduct"
// }
// ]
// },
// {
// "groupName": "Intermediate Data Product",
// "groupId": "group_g007",
// "nodes": [
// {
// "id": "dataProduct_27",
// "name": "Underwriting",
// "type": "DataProduct"
// }
// ]
// },
// {
// "groupName": "Source Aligned Data Product",
// "groupId": "group_g008",
// "nodes": [
// {
// "id": "dataProduct_28",
// "name": "Claims Fraud",
// "type": "DataProduct"
// },
// {
// "id": "dataProduct_29",
// "name": "Customer Eng",
// "type": "DataProduct"
// },
// {
// "id": "dataProduct_30",
// "name": "Predictive",
// "type": "DataProduct"
// },
// {
// "id": "dataProduct_31",
// "name": "Risk Analysis",
// "type": "DataProduct"
// }
// ]
// }
// ],
// "relationships": [
// {
// "source": "domain_1",
// "target": "subdomain_2"
// },
// {
// "source": "domain_1",
// "target": "subdomain_3"
// },
// {
// "source": "domain_1",
// "target": "dataProduct_25"//Telematics
// },
// {
// "source": "domain_1",
// "target": "dataProduct_25"
// },
// {
// "source": "domain_1",
// "target": "dataProduct_26"//Life Expectancy
// },
// {
// "source": "domain_1",
// "target": "dataProduct_27"//Underwriting
// },
// {
// "source": "subdomain_2", //Life Insurrance
// "target": "dataProduct_21"//PolicyHolder
// },
// {
// "source": "subdomain_2", //Life Insurrance
// "target": "dataProduct_22"//Cust Sa
// },
// {
// "source": "subdomain_2", //Life Insurrance
// "target": "dataProduct_23"//Fraud
// },
// {
// "source": "subdomain_2", //Life Insurrance
// "target": "dataProduct_24"//Risk
// },
// {
// "source": "subdomain_3", //Insurance Term
// "target": "dataProduct_28"//Claims fraud
// },
// {
// "source": "subdomain_3", //Insurance Term
// "target": "dataProduct_29"//Cust Eng
// },
// {
// "source": "subdomain_3", //Insurance Term
// "target": "dataProduct_30"//Pred
// },
// {
// "source": "subdomain_3", //Insurance Term
// "target": "dataProduct_31"//Risk Analysis
// }
// ]
// // "groups": [
// // {
// // "groupId": "DOMAIN-1",
// // "groupName": "Domain",
// // "nodes": [
// // {
// // "id": "1",
// // "name": "Insurance",
// // "type": "Domain"
// // }
// // ]
// // },
// // {
// // "groupId": "CONSUMPTION-3175-09",
// // "groupName": "Consumption Aligned Data Product",
// // "nodes": [
// // {
// // "id": "13",
// // "name": "Credit risk score",
// // "type": "Data Product"
// // }
// // ]
// // },
// // {
// // "groupId": "SUBDOMAIN-7538-02",
// // "groupName": "Subdomain",
// // "nodes": [
// // {
// // "id": "2",
// // "name": "Life Insurance",
// // "type": "Sub Domain"
// // },
// // {
// // "id": "3",
// // "name": "Insurance Term",
// // "type": "Sub Domain"
// // }
// // ]
// // },
// // {
// // "groupId": "CONSUMPTION-7262-06",
// // "groupName": "Consumption Aligned Data Product",
// // "nodes": [
// // {
// // "id": "11",
// // "name": "Health Analytics",
// // "type": "Data Product"
// // }
// // ]
// // },
// // {
// // "groupId": "INTERMEDIATE-5122-02",
// // "groupName": "Intermediate Data Product",
// // "nodes": [
// // {
// // "id": "12",
// // "name": "Underwriting",
// // "type": "Data Product"
// // }
// // ]
// // }
// // ],
// // "relationships": [
// // {
// // "source": "2",
// // "target": "13"
// // },
// // {
// // "source": "1",
// // "target": "2"
// // },
// // {
// // "source": "1",
// // "target": "3"
// // },
// // {
// // "source": "1",
// // "target": "11"
// // },
// // {
// // "source": "1",
// // "target": "12"
// // }
// // ]
//
async ngOnInit(): Promise {
console.log("sssss------>",this.data);
await this.initiateDataLineageGraph(this.data);

}

async initiateDataLineageGraph(incomingData:any){
console.log("sssss------>",incomingData);

this.dataLineageTitle = 'Data Lineage';
this.dataLineageForType = incomingData.dataLineageType;
this.dataLineageForValue = incomingData.dataLineageTypeName;
this.sampleApiResponse = incomingData.dataLineageData;
await this.convertIntoNgxVflowFormat(this.sampleApiResponse);
this.cdr.detectChanges();
// this.vflow.update();

}

//Service Integration **
getdataLineageData(){
this.dataLineageService.getDataLineageDetails()
.subscribe((data: any) => {
console.log("called", data);
});
}

convertIntoNgxVflowFormat(apiResponse: any) {
if(apiResponse && apiResponse.groups && Array.isArray(apiResponse.groups) && apiResponse.groups.length > 0){
const lineageGroups = apiResponse.groups;
let groupNodeCount = 0;

  // Define grid parameters
  const cellWidth = 300; // Width of each cell
  const cellHeight = 300; // Height of each cell
  const gap = 50; // Gap between cells

  lineageGroups.forEach((group: any, index: number) => {
    if(group.nodes && Array.isArray(group.nodes) && group.nodes.length > 0){
      groupNodeCount = group.nodes.length;
      group.nodes.forEach((node: any, nodeIndex: number) => this.processNode(node, group.groupId, nodeIndex));
    }
    const groupNameLength = group.groupName.length;
    const calculatedWidth = groupNameLength * 10 + 5;
    const nodeWidth = Math.max(calculatedWidth, 260);
    const nodeHeight = 90 + 48 * groupNodeCount;

    // Calculate grid position
    const row = Math.floor(index / 5); // Assuming 5 groups per row
    const col = index % 5;
    const x = col * (cellWidth + gap);
    const y = row * (cellHeight + gap);

    console.log("groupNameLength >>>",groupNameLength);
    //write code for position here handleTypeForGroupUse
    // if(this.handleTypeForGroupUse == 'target'){
    //   xOfGroup = (100 + nodeWidth) + 30
    // }

    const signal: WritableSignal<Point> = {
      value: { x: x, y: y },
      // other properties and methods
    };

    this.nodes.push({
      id: group.groupId,
      point: signal.value,
      width: nodeWidth,
      height: nodeHeight,
      type: 'template-group',
      data: { 
        groupName: group.groupName,
        nodeCount: groupNodeCount
      } as any,
    });
  });
}

 if(apiResponse && apiResponse.relationships && Array.isArray(apiResponse.relationships) && apiResponse.relationships.length > 0){
  console.log('Single edge------>', apiResponse.relationships);
  const lineageEdges = apiResponse.relationships;

  lineageEdges.forEach((edge:any) => {
    this.edges.push({
      id: `${edge.source} -> ${edge.target}`,
      source: `${edge.source}`,
      target: `${edge.target}`,
      type: 'template',
      data: { strokeWidth: 1.5, color: '#0892d0', animation: true },
      markers: { 
        end: { 
          type: 'arrow-closed', 
          width: 14, 
          height: 14, 
          color: '#0892d0' 
        } 
      },
    });
  })
}
console.log("nodes-------->", this.nodes);
console.log("edges-------->", this.edges);

}

processNode(node: any, parentId: string, nodeIndex: number) {
const handleType = this.determineHandleType(node); // make this global to use in group positioning
this.handleTypeForGroupUse = handleType;
const y = 50 + ((nodeIndex * 48) + 26);

// Ensure WritableSignal<{ x: number; y: number; }> includes x and y
const signal: WritableSignal<Point> = {
  value: { x: 24, y: y },
  // other properties and methods
};

this.nodes.push({
  id: node.id,
  point: signal.value,
  type: CustomNodeForLineageComponent,
  data: { 
    nodeName: node.name,
    handleType: handleType,
  },
  parentId: parentId,
});

}

determineHandleType(node: any): "source" | "both" | "target" | undefined {
if (this.sampleApiResponse && this.sampleApiResponse['relationships'] && Array.isArray(this.sampleApiResponse['relationships']) && this.sampleApiResponse['relationships'].length > 0) {
const sourceSet = new Set(this.sampleApiResponse['relationships'].map((rel: any) => rel.source));
const targetSet = new Set(this.sampleApiResponse['relationships'].map((rel: any) => rel.target));

    const isSource = sourceSet.has(node.id);
    const isTarget = targetSet.has(node.id);

    if (isSource && isTarget) {
        return 'both';
    } else if (isSource) {
        return 'source';
    } else if (isTarget) {
        return 'target';
    }
    return undefined;
}
// Ensure a return value if the if condition is not met
return undefined;

}

//
handleComponentEvent(
event: ComponentNodeEvent<[CustomNodeForLineageComponent]>
) {
if (event.eventName === 'redSquareEvent') {
console.log(event.eventPayload);
}
}

private simulationNodes = this.nodes.map((n) => {
return {
id: n.id,

  get x() {
    return n.point().x
  },

  set x(x: number) {
    n.point.update(state => ({ ...state, x }))
  },

  get y() {
    return n.point().y
  },

  set y(y: number) {
    n.point.update(state => ({ ...state, y }))
  },
}

})

private linkForce = d3
.forceLink(this.edges.map(e => ({ source: e.source, target: e.target })))
// @ts-ignore
.id((d) => d.id)
.distance(50)

private simulation = d3.forceSimulation()
.force("charge", d3.forceManyBody().strength(-500))
.force("x", d3.forceX())
.force("y", d3.forceY())
// center of viewport
.force("center", d3.forceCenter(100, 100))
.nodes(this.simulationNodes)
.force("link", this.linkForce)

protected onDistanceChange(event: Event) {
const distance = +(event.target as HTMLInputElement).value

this.linkForce.distance(distance)
this.simulation.alpha(.5).restart()

setTimeout(() => {
  this.vflow.fitView({ duration: 500 })
}, 50);

}

}

function randomHex() {
const hexValues = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 'A', 'B', 'C', 'D', 'E', 'F'];

let hex = '#';

for (let i = 0; i < 6; i++) {
const index = Math.floor(Math.random() * hexValues.length)
hex += hexValues[index];
}

return hex
}
`

Custom Node HTML :

`

<div 
  (click)="onClick()"
  class="custom-node" 
  selectable 
  [class.custom-node_selected]="selected()">
  <span class="custom-node__icon">
    <img src="./../../../../assets/images/Domain Mask (4).svg" alt="domain icon">
  </span>
  <span class="custom-node__text" matTooltip="{{ data()?.nodeName }}">
    {{ data()?.nodeName }}
  </span>
  @for(handle of getHandlesData(data()?.handleType); track handle){
    <!-- <span>{{handle.type}}</span> -->
    <handle class="custom-handle" [type]="getHandleType(handle.type)" [position]="getHandlePosition(handle.position)"/>
  }

  <!-- <handle *ngFor="let handle of handles" [type]="getHandleType(data()?.handleType)" [position]="getHandlePosition(data()?.position)"/> -->
</div>
  <!-- this method returns values which we are passing it in data from parent component -->
  <!-- we are getting above from parent but we have define its type in custom component -->`
  
  Custom Node Ts :
 `

import { CommonModule } from '@angular/common';
import { ChangeDetectionStrategy, Component, EventEmitter, NO_ERRORS_SCHEMA, OnInit, Output, inject } from '@angular/core';
import { MatTooltipModule } from '@angular/material/tooltip';
import { CustomNodeComponent, ComponentNodeEvent, Edge, Node, VflowModule } from 'ngx-vflow';

export interface CustomNodeData {
nodeName: string;
handleType: string;
// handleType: HandleType;
// position: PositionType;
}

type HandleType = 'source' | 'target' | 'both';
// type PositionType = 'left' | 'right';

@component({
selector: 'app-custom-node',
standalone: true,
imports: [VflowModule, MatTooltipModule, CommonModule],
templateUrl: './custom-node.component.html',
styleUrl: './custom-node.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
schemas: [NO_ERRORS_SCHEMA]
})

export class CustomNodeForLineageComponent extends CustomNodeComponent{
@output() redSquareEvent = new EventEmitter()
// handleType : HandleType = 'source';

onClick() {
this.redSquareEvent.emit('Click from red square')
}

getHandleType(handleType: string | undefined): 'source' | 'target' {
return handleType === 'source' || handleType === 'target' ? handleType : 'source';
}

getHandlePosition(position: string | undefined): 'left' | 'right' {
return position === 'left' || position === 'right' ? position : 'left';
}

getHandlesData(handleType: string | undefined) {
console.log('called');

if(handleType === 'source'){
  return [{ type: 'source', position: 'right' }];
}
else if(handleType === 'target'){
  return [{ type: 'target', position: 'left' }];
}
else{
  return [
    { type: 'source', position: 'right' },
    { type: 'target', position: 'left' }
  ];
}

}

}
export { CustomNodeComponent };
`
Getting this error
www

@artem-mangilev
Copy link
Owner

Hi! Can I see the repo with this code, or maybe you could create a stackblitz example with the issue?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants