feat: add graph framework

This commit is contained in:
Michal Szczepanski 2024-07-25 01:34:09 +02:00
parent 896b012c71
commit 93b7cd3959
4 changed files with 347 additions and 47 deletions

View File

@ -20,6 +20,7 @@
"@angular/platform-browser-dynamic": "^18.1.0", "@angular/platform-browser-dynamic": "^18.1.0",
"@angular/router": "^18.1.0", "@angular/router": "^18.1.0",
"@codemirror/lang-python": "^6.1.6", "@codemirror/lang-python": "^6.1.6",
"@kr0san89/ngx-graph": "^11.0.0",
"@xyflow/system": "^0.0.37", "@xyflow/system": "^0.0.37",
"codemirror": "^6.0.1", "codemirror": "^6.0.1",
"codemirror-extension-inline-suggestion": "^0.0.3", "codemirror-extension-inline-suggestion": "^0.0.3",
@ -3957,6 +3958,35 @@
"tslib": "2" "tslib": "2"
} }
}, },
"node_modules/@kr0san89/ngx-graph": {
"version": "11.0.0",
"resolved": "https://npm.fnexe.com/@kr0san89/ngx-graph/-/ngx-graph-11.0.0.tgz",
"integrity": "sha512-MMVxJ+ShZqnva7qerDk/M0xtyz4MzVoSsQRo0DH9DFvKtuG99SHhw9+vPtDJl8oomN04PACyzTufY/fCT63lZg==",
"license": "MIT",
"dependencies": {
"d3-color": "^3.1.0",
"d3-dispatch": "^3.0.1",
"d3-ease": "^3.0.1",
"d3-force": "^3.0.0",
"d3-interpolate": "3.0.1",
"d3-scale": "4.0.2",
"d3-selection": "^3.0.0",
"d3-shape": "^3.2.0",
"d3-timer": "^3.0.1",
"d3-transition": "^3.0.1",
"dagre": "^0.8.4",
"transformation-matrix": "^1.15.3",
"tslib": "^2.0.0",
"webcola": "^3.3.8"
},
"peerDependencies": {
"@angular/animations": "18.x",
"@angular/cdk": "18.x",
"@angular/common": "18.x",
"@angular/core": "18.x",
"rxjs": "6.x || 7.x"
}
},
"node_modules/@leichtgewicht/ip-codec": { "node_modules/@leichtgewicht/ip-codec": {
"version": "2.0.5", "version": "2.0.5",
"resolved": "https://npm.fnexe.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", "resolved": "https://npm.fnexe.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz",
@ -8482,6 +8512,18 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/d3-array": {
"version": "3.2.4",
"resolved": "https://npm.fnexe.com/d3-array/-/d3-array-3.2.4.tgz",
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
"license": "ISC",
"dependencies": {
"internmap": "1 - 2"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-color": { "node_modules/d3-color": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://npm.fnexe.com/d3-color/-/d3-color-3.1.0.tgz", "resolved": "https://npm.fnexe.com/d3-color/-/d3-color-3.1.0.tgz",
@ -8522,6 +8564,29 @@
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/d3-force": {
"version": "3.0.0",
"resolved": "https://npm.fnexe.com/d3-force/-/d3-force-3.0.0.tgz",
"integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==",
"license": "ISC",
"dependencies": {
"d3-dispatch": "1 - 3",
"d3-quadtree": "1 - 3",
"d3-timer": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-format": {
"version": "3.1.0",
"resolved": "https://npm.fnexe.com/d3-format/-/d3-format-3.1.0.tgz",
"integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-interpolate": { "node_modules/d3-interpolate": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://npm.fnexe.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz", "resolved": "https://npm.fnexe.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
@ -8534,6 +8599,40 @@
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/d3-path": {
"version": "3.1.0",
"resolved": "https://npm.fnexe.com/d3-path/-/d3-path-3.1.0.tgz",
"integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-quadtree": {
"version": "3.0.1",
"resolved": "https://npm.fnexe.com/d3-quadtree/-/d3-quadtree-3.0.1.tgz",
"integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/d3-scale": {
"version": "4.0.2",
"resolved": "https://npm.fnexe.com/d3-scale/-/d3-scale-4.0.2.tgz",
"integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
"license": "ISC",
"dependencies": {
"d3-array": "2.10.0 - 3",
"d3-format": "1 - 3",
"d3-interpolate": "1.2.0 - 3",
"d3-time": "2.1.1 - 3",
"d3-time-format": "2 - 4"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-selection": { "node_modules/d3-selection": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://npm.fnexe.com/d3-selection/-/d3-selection-3.0.0.tgz", "resolved": "https://npm.fnexe.com/d3-selection/-/d3-selection-3.0.0.tgz",
@ -8543,6 +8642,42 @@
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/d3-shape": {
"version": "3.2.0",
"resolved": "https://npm.fnexe.com/d3-shape/-/d3-shape-3.2.0.tgz",
"integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
"license": "ISC",
"dependencies": {
"d3-path": "^3.1.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-time": {
"version": "3.1.0",
"resolved": "https://npm.fnexe.com/d3-time/-/d3-time-3.1.0.tgz",
"integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
"license": "ISC",
"dependencies": {
"d3-array": "2 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-time-format": {
"version": "4.1.0",
"resolved": "https://npm.fnexe.com/d3-time-format/-/d3-time-format-4.1.0.tgz",
"integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
"license": "ISC",
"dependencies": {
"d3-time": "1 - 3"
},
"engines": {
"node": ">=12"
}
},
"node_modules/d3-timer": { "node_modules/d3-timer": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://npm.fnexe.com/d3-timer/-/d3-timer-3.0.1.tgz", "resolved": "https://npm.fnexe.com/d3-timer/-/d3-timer-3.0.1.tgz",
@ -8587,6 +8722,16 @@
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/dagre": {
"version": "0.8.5",
"resolved": "https://npm.fnexe.com/dagre/-/dagre-0.8.5.tgz",
"integrity": "sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==",
"license": "MIT",
"dependencies": {
"graphlib": "^2.1.8",
"lodash": "^4.17.15"
}
},
"node_modules/date-format": { "node_modules/date-format": {
"version": "4.0.14", "version": "4.0.14",
"resolved": "https://npm.fnexe.com/date-format/-/date-format-4.0.14.tgz", "resolved": "https://npm.fnexe.com/date-format/-/date-format-4.0.14.tgz",
@ -10515,6 +10660,15 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/graphlib": {
"version": "2.1.8",
"resolved": "https://npm.fnexe.com/graphlib/-/graphlib-2.1.8.tgz",
"integrity": "sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==",
"license": "MIT",
"dependencies": {
"lodash": "^4.17.15"
}
},
"node_modules/handle-thing": { "node_modules/handle-thing": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://npm.fnexe.com/handle-thing/-/handle-thing-2.0.1.tgz", "resolved": "https://npm.fnexe.com/handle-thing/-/handle-thing-2.0.1.tgz",
@ -11005,6 +11159,15 @@
"node": "^14.17.0 || ^16.13.0 || >=18.0.0" "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
} }
}, },
"node_modules/internmap": {
"version": "2.0.3",
"resolved": "https://npm.fnexe.com/internmap/-/internmap-2.0.3.tgz",
"integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
"license": "ISC",
"engines": {
"node": ">=12"
}
},
"node_modules/ip-address": { "node_modules/ip-address": {
"version": "9.0.5", "version": "9.0.5",
"resolved": "https://npm.fnexe.com/ip-address/-/ip-address-9.0.5.tgz", "resolved": "https://npm.fnexe.com/ip-address/-/ip-address-9.0.5.tgz",
@ -12396,7 +12559,6 @@
"version": "4.17.21", "version": "4.17.21",
"resolved": "https://npm.fnexe.com/lodash/-/lodash-4.17.21.tgz", "resolved": "https://npm.fnexe.com/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/lodash.debounce": { "node_modules/lodash.debounce": {
@ -16664,6 +16826,12 @@
"node": ">=0.6" "node": ">=0.6"
} }
}, },
"node_modules/transformation-matrix": {
"version": "1.15.3",
"resolved": "https://npm.fnexe.com/transformation-matrix/-/transformation-matrix-1.15.3.tgz",
"integrity": "sha512-ThJH58GNFKhCw3gIoOtwf3tNwuYjbyEeiGdeq4mNMYWdJctnI896KUqn6PVt7jmNVepqa1bcKQtnMB1HtjsDMA==",
"license": "MIT"
},
"node_modules/tree-dump": { "node_modules/tree-dump": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://npm.fnexe.com/tree-dump/-/tree-dump-1.0.2.tgz", "resolved": "https://npm.fnexe.com/tree-dump/-/tree-dump-1.0.2.tgz",
@ -17191,6 +17359,61 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/webcola": {
"version": "3.4.0",
"resolved": "https://npm.fnexe.com/webcola/-/webcola-3.4.0.tgz",
"integrity": "sha512-4BiLXjXw3SJHo3Xd+rF+7fyClT6n7I+AR6TkBqyQ4kTsePSAMDLRCXY1f3B/kXJeP9tYn4G1TblxTO+jAt0gaw==",
"license": "MIT",
"dependencies": {
"d3-dispatch": "^1.0.3",
"d3-drag": "^1.0.4",
"d3-shape": "^1.3.5",
"d3-timer": "^1.0.5"
}
},
"node_modules/webcola/node_modules/d3-dispatch": {
"version": "1.0.6",
"resolved": "https://npm.fnexe.com/d3-dispatch/-/d3-dispatch-1.0.6.tgz",
"integrity": "sha512-fVjoElzjhCEy+Hbn8KygnmMS7Or0a9sI2UzGwoB7cCtvI1XpVN9GpoYlnb3xt2YV66oXYb1fLJ8GMvP4hdU1RA==",
"license": "BSD-3-Clause"
},
"node_modules/webcola/node_modules/d3-drag": {
"version": "1.2.5",
"resolved": "https://npm.fnexe.com/d3-drag/-/d3-drag-1.2.5.tgz",
"integrity": "sha512-rD1ohlkKQwMZYkQlYVCrSFxsWPzI97+W+PaEIBNTMxRuxz9RF0Hi5nJWHGVJ3Om9d2fRTe1yOBINJyy/ahV95w==",
"license": "BSD-3-Clause",
"dependencies": {
"d3-dispatch": "1",
"d3-selection": "1"
}
},
"node_modules/webcola/node_modules/d3-path": {
"version": "1.0.9",
"resolved": "https://npm.fnexe.com/d3-path/-/d3-path-1.0.9.tgz",
"integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==",
"license": "BSD-3-Clause"
},
"node_modules/webcola/node_modules/d3-selection": {
"version": "1.4.2",
"resolved": "https://npm.fnexe.com/d3-selection/-/d3-selection-1.4.2.tgz",
"integrity": "sha512-SJ0BqYihzOjDnnlfyeHT0e30k0K1+5sR3d5fNueCNeuhZTnGw4M4o8mqJchSwgKMXCNFo+e2VTChiSJ0vYtXkg==",
"license": "BSD-3-Clause"
},
"node_modules/webcola/node_modules/d3-shape": {
"version": "1.3.7",
"resolved": "https://npm.fnexe.com/d3-shape/-/d3-shape-1.3.7.tgz",
"integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==",
"license": "BSD-3-Clause",
"dependencies": {
"d3-path": "1"
}
},
"node_modules/webcola/node_modules/d3-timer": {
"version": "1.0.10",
"resolved": "https://npm.fnexe.com/d3-timer/-/d3-timer-1.0.10.tgz",
"integrity": "sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw==",
"license": "BSD-3-Clause"
},
"node_modules/webpack": { "node_modules/webpack": {
"version": "5.92.1", "version": "5.92.1",
"resolved": "https://npm.fnexe.com/webpack/-/webpack-5.92.1.tgz", "resolved": "https://npm.fnexe.com/webpack/-/webpack-5.92.1.tgz",

View File

@ -32,6 +32,7 @@
"@angular/platform-browser-dynamic": "^18.1.0", "@angular/platform-browser-dynamic": "^18.1.0",
"@angular/router": "^18.1.0", "@angular/router": "^18.1.0",
"@codemirror/lang-python": "^6.1.6", "@codemirror/lang-python": "^6.1.6",
"@kr0san89/ngx-graph": "^11.0.0",
"@xyflow/system": "^0.0.37", "@xyflow/system": "^0.0.37",
"codemirror": "^6.0.1", "codemirror": "^6.0.1",
"codemirror-extension-inline-suggestion": "^0.0.3", "codemirror-extension-inline-suggestion": "^0.0.3",

View File

@ -1,21 +1,122 @@
<div> <div>
<h1>Slow shit</h1> <h1>Slow shit</h1>
<app-graph-component></app-graph-component> <app-graph-component></app-graph-component>
<div style="display: inline-block;position: relative"> <ngx-graph
<div style="width: 500px;height: 300px;border:1px red solid; overflow: auto;" #parent (scroll)="handleMoved()"> class="chart-container"
<div #source style="border:1px green solid;width: fit-content;margin-left:200px;margin-top:50px" cdkDrag (cdkDragMoved)="handleMoved()"> [view]="[800, 550]"
I'm a div inside a SVG. [links]="[
</div> {
<div #target style="border:1px green solid;width: fit-content;margin-left:10px;margin-top:120px" cdkDrag (cdkDragMoved)="handleMoved()"> id: 'a',
I'm a second div inside a SVG. source: 'first',
</div> target: 'second',
<svg width="100%" height="100%" style="position: absolute;top:0px;left:0px;right:0;bottom:0;pointer-events: none;" xmlns="http://www.w3.org/2000/svg"> label: 'is parent of'
<g> }, {
<path #path stroke="black" stroke-width="2" fill="transparent"/> id: 'b',
</g> source: 'first',
</svg> target: 'c1',
</div> label: 'custom label'
</div> }, {
id: 'd',
source: 'first',
target: 'c2',
label: 'custom label'
}, {
id: 'e',
source: 'c1',
target: 'd',
label: 'first link'
}, {
id: 'f',
source: 'c1',
target: 'd',
label: 'second link'
}
]"
[nodes]="[
{
id: 'first',
label: 'A'
}, {
id: 'second',
label: 'B'
}, {
id: 'c1',
label: 'C1'
}, {
id: 'c2',
label: 'C2'
}, {
id: 'd',
label: 'D'
}
]"
[clusters]="[
{
id: 'third',
label: 'Cluster node',
childNodeIds: ['c1', 'c2']
}
]"
layout="dagreCluster"
(select)="handleNodeSelect($event)"
>
<ng-template #defsTemplate>
<svg:marker id="arrow" viewBox="0 -5 10 10" refX="8" refY="0" markerWidth="4" markerHeight="4" orient="auto">
<svg:path d="M0,-5L10,0L0,5" class="arrow-head" />
</svg:marker>
</ng-template>
<ng-template #clusterTemplate let-cluster>
<svg:g class="node cluster">
<svg:rect
rx="5"
ry="5"
[attr.width]="cluster.dimension.width"
[attr.height]="cluster.dimension.height"
[attr.fill]="cluster.data.color"
/>
</svg:g>
</ng-template>
<ng-template #nodeTemplate let-node>
<svg:g class="node">
<svg:rect
[attr.width]="node.dimension.width"
[attr.height]="node.dimension.height"
[attr.fill]="node.data.color"
/>
<svg:text alignment-baseline="central" [attr.x]="10" [attr.y]="node.dimension.height / 2">
{{node.label}}
</svg:text>
<!--<foreignObject width="100%" height="100%">
<xhtml:div>
<xhtml:div #source style="border:1px green solid;width: fit-content;margin-left:200px;margin-top:50px;">
I'm a div inside a SVG.
</xhtml:div>
<xhtml:p #target style="border:1px green solid;width: fit-content;margin-left:10px;margin-top:120px">
I'm a second div inside a SVG.
</xhtml:p>
</xhtml:div>
</foreignObject>-->
</svg:g>
</ng-template>
<ng-template #linkTemplate let-link>
<svg:g class="edge">
<svg:path class="line" stroke-width="2" marker-end="url(#arrow)"></svg:path>
<svg:text class="edge-label" text-anchor="middle">
<textPath
class="text-path"
[attr.href]="'#' + link.id"
[style.dominant-baseline]="link.dominantBaseline"
startOffset="50%"
>
{{link.label}}
</textPath>
</svg:text>
</svg:g>
</ng-template>
</ngx-graph>
</div> </div>
<router-outlet /> <router-outlet />

View File

@ -1,44 +1,19 @@
import {AfterViewInit, Component, ViewChild} from '@angular/core'; import {Component} from "@angular/core";
import { RouterOutlet } from '@angular/router'; import { RouterOutlet } from '@angular/router';
import {GraphModule} from "./view/graph/graph.module"; import {GraphModule} from "./view/graph/graph.module";
import { getBezierPath } from '@xyflow/system'; import {NgxGraphModule} from "@kr0san89/ngx-graph";
import {DragDropModule} from '@angular/cdk/drag-drop';
import {ChildView} from "./model/ViewModel";
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
standalone: true, standalone: true,
imports: [RouterOutlet, GraphModule, DragDropModule], imports: [RouterOutlet, GraphModule, NgxGraphModule],
templateUrl: './app.component.html', templateUrl: './app.component.html',
styleUrl: './app.component.scss' styleUrl: './app.component.scss'
}) })
export class AppComponent implements AfterViewInit { export class AppComponent {
@ViewChild('source') source?: ChildView
@ViewChild('target') target?: ChildView
@ViewChild('parent') parent?: ChildView
@ViewChild('path') path?: ChildView<SVGPathElement>
ngAfterViewInit() { handleNodeSelect = (event: any) => {
if(!this.source || !this.target || !this.parent || !this.path) return console.log('handleNodeSelect', event)
this.drawEdge(this.source.nativeElement, this.target.nativeElement, this.parent.nativeElement, this.path.nativeElement)
}
handleMoved = () => {
if(!this.source || !this.target || !this.parent || !this.path) return
this.drawEdge(this.source.nativeElement, this.target.nativeElement, this.parent.nativeElement, this.path.nativeElement)
}
private drawEdge(source: HTMLDivElement, target: HTMLDivElement, parent: HTMLDivElement, edge: SVGPathElement) {
const sourceRect = source.getBoundingClientRect()
const targetRect = target.getBoundingClientRect()
const parentRect = parent.getBoundingClientRect()
const path = getBezierPath({
sourceX: sourceRect.x - parentRect.x + (sourceRect.width / 2),
sourceY: sourceRect.y - parentRect.y - 1 + sourceRect.height,
targetX: targetRect.x - parentRect.x + (targetRect.width / 2),
targetY: targetRect.y - parentRect.y
})
edge.setAttribute('d', path[0])
} }
} }