diff --git a/slizaa-web/app/package.json b/slizaa-web/app/package.json index a9d975c..5837f82 100644 --- a/slizaa-web/app/package.json +++ b/slizaa-web/app/package.json @@ -13,6 +13,7 @@ "apollo-boost": "^0.1.22", "d3": "^5.9.1", "d3-scale": "^2.2.2", + "elkjs": "^0.6.2", "global": "^4.3.2", "graphql": "14.0.2 - 14.2.0 || ^14.3.1", "graphql-tag": "^2.10.0", diff --git a/slizaa-web/app/src/components/dependencygraph/DependencyGraph.tsx b/slizaa-web/app/src/components/dependencygraph/DependencyGraph.tsx new file mode 100644 index 0000000..daac106 --- /dev/null +++ b/slizaa-web/app/src/components/dependencygraph/DependencyGraph.tsx @@ -0,0 +1,257 @@ +import ELK, {ElkExtendedEdge, ElkNode, ElkPoint} from 'elkjs/lib/elk.bundled.js' +import * as React from "react"; +import {setupCanvas} from "../dsm/DpiFixer"; +import {ISlizaaDependencyListState} from "./IDependencyGraphState"; + +export class DependencyGraph extends React.Component { + + private readonly CORNER_RADIUS = 3; + private readonly NODE_HEIGHT = 30; + private readonly NODE_WIDTH = 170; + + private canvasRef: HTMLCanvasElement | null; + private renderingContext: CanvasRenderingContext2D | null; + + private rootNode: ElkNode | null; + + constructor(props: any) { + super(props); + + this.state = {}; + } + + public componentDidMount(): void { + + if (this.canvasRef) { + this.renderingContext = this.canvasRef.getContext("2d") + } + + const elk = new ELK(); + // tslint:disable:object-literal-sort-keys + const graph = { + id: "root", + layoutOptions: { + 'elk.algorithm': 'layered', + 'org.eclipse.elk.direction': 'DOWN', + 'org.eclipse.elk.layered.spacing.edgeNodeBetweenLayers': '20', + 'org.eclipse.elk.layered.nodePlacement.strategy': 'BRANDES_KOEPF', + }, + children: [ + {id: "n1", width: this.NODE_WIDTH, height: this.NODE_HEIGHT}, + {id: "n2", width: this.NODE_WIDTH, height: this.NODE_HEIGHT}, + {id: "n3", width: this.NODE_WIDTH, height: this.NODE_HEIGHT}, + {id: "n4", width: this.NODE_WIDTH, height: this.NODE_HEIGHT}, + {id: "n5", width: this.NODE_WIDTH, height: this.NODE_HEIGHT}, + {id: "n6", width: this.NODE_WIDTH, height: this.NODE_HEIGHT}, + ], + edges: [ + {id: "e1", sources: ["n1"], targets: ["n6"]}, + {id: "e2", sources: ["n2"], targets: ["n6"]}, + {id: "e3", sources: ["n3"], targets: ["n1"]}, + {id: "e4", sources: ["n2"], targets: ["n6"]}, + {id: "e5", sources: ["n4"], targets: ["n5"]}, + {id: "e6", sources: ["n6"], targets: ["n3"]}, + {id: "e7", sources: ["n2"], targets: ["n6"]}, + {id: "e8", sources: ["n2"], targets: ["n5"]} + ] + } + + elk.layout(graph) + // tslint:disable-next-line:no-console + .then((value) => { + this.rootNode = value; + this.draw(); + }) + // tslint:disable-next-line:no-console + .catch(console.error) + } + + public render() { + return
+ (this.canvasRef = ref)}/> +
+ } + + private draw = () => { + + if (this.canvasRef && this.renderingContext && this.rootNode && this.rootNode.width && this.rootNode.height) { + + const renderingContext = this.renderingContext; + const scale = 1.6; + + // tslint:disable-next-line:no-console + console.log(JSON.stringify(this.rootNode)) + + setupCanvas(this.canvasRef, this.renderingContext, this.rootNode.width * scale, this.rootNode.height * scale); + renderingContext.scale(scale, scale) + + // draw the nodes + if (this.rootNode.children) { + this.rootNode.children.forEach(node => { + this.drawNode(renderingContext, node); + } + ) + } + + + // draw the edges + if (this.rootNode.edges) { + this.rootNode.edges.forEach(edge => { + + const extendedEdge: ElkExtendedEdge = edge as ElkExtendedEdge; + + extendedEdge.sections.forEach(section => { + + renderingContext.strokeStyle = section.startPoint.y > section.endPoint.y ? "#FF0000" : "#000000"; + renderingContext.fillStyle = section.startPoint.y > section.endPoint.y ? "#FF0000" : "#000000"; + + let lastPoint = section.startPoint; + + renderingContext.beginPath(); + renderingContext.moveTo(section.startPoint.x, section.startPoint.y); + + // + if (section.bendPoints) { + + // tslint:disable-next-line:prefer-for-of + for (let i = 0; i < section.bendPoints.length; i++) { + + const currentPoint = section.bendPoints[i]; + const nextPoint = i < section.bendPoints.length - 1 ? section.bendPoints[i + 1] : section.endPoint; + + const lastDeltaX = currentPoint.x - lastPoint.x; + const lastDeltaY = currentPoint.y - lastPoint.y; + const nextDeltaX = nextPoint.x - currentPoint.x; + const nextDeltaY = nextPoint.y - currentPoint.y; + + if (lastDeltaX !== 0) { + renderingContext.lineTo(lastDeltaX > 0 ? currentPoint.x - this.CORNER_RADIUS : currentPoint.x + this.CORNER_RADIUS, currentPoint.y); + renderingContext.quadraticCurveTo(currentPoint.x, currentPoint.y, currentPoint.x, nextDeltaY < 0 ? currentPoint.y - this.CORNER_RADIUS : currentPoint.y + this.CORNER_RADIUS); + } else if (lastDeltaY !== 0) { + renderingContext.lineTo(currentPoint.x, lastDeltaY > 0 ? currentPoint.y - this.CORNER_RADIUS : currentPoint.y + this.CORNER_RADIUS); + renderingContext.quadraticCurveTo(currentPoint.x, currentPoint.y, nextDeltaX < 0 ? currentPoint.x - this.CORNER_RADIUS : currentPoint.x + this.CORNER_RADIUS, currentPoint.y); + } + + lastPoint = currentPoint; + } + } + + renderingContext.lineTo(section.endPoint.x, section.endPoint.y); + renderingContext.stroke(); + + this.drawArrowhead(renderingContext, lastPoint, section.endPoint, 4); + }) + } + ) + } + } + } + + private drawNode = (context: CanvasRenderingContext2D, node: ElkNode) => { + + if (node.x && node.y && node.width && node.height) { + + context.save(); + + this.roundRect( + context, + node.x, + node.y, + node.width, + node.height, + this.CORNER_RADIUS + ); + + // ...set the clipping area + context.beginPath(); + context.rect(node.x, + node.y, + node.width, + node.height,); + context.clip(); + + context.textAlign = "left"; + context.textBaseline = "middle"; + context.fillText("i.c.s.h.model", node.x + (this.NODE_HEIGHT / 2) , node.y + (this.NODE_HEIGHT / 2) ); + + // if (node.children) { + // node.children.forEach(child => { + // this.drawNode(context, node); + // } + // ) + // } + + context.restore(); + } + } + + /** + * Draw an arrowhead on a line on an HTML5 canvas. + * + * Based almost entirely off of http://stackoverflow.com/a/36805543/281460 with some modifications + * for readability and ease of use. + * + * @param context The drawing context on which to put the arrowhead. + * @param from A point, specified as an object with 'x' and 'y' properties, where the arrow starts + * (not the arrowhead, the arrow itself). + * @param to A point, specified as an object with 'x' and 'y' properties, where the arrow ends + * (not the arrowhead, the arrow itself). + * @param radius The radius of the arrowhead. This controls how "thick" the arrowhead looks. + */ + private drawArrowhead = (context: CanvasRenderingContext2D, from: ElkPoint, to: ElkPoint, radius: number) => { + + const xDelta = from.x - to.x; + const yDelta = from.y - to.y; + + const xCenter = xDelta !== 0 ? (xDelta > 0 ? to.x + 5 : to.x - 5) : to.x; + const yCenter = yDelta !== 0 ? (yDelta > 0 ? to.y + 5 : to.y - 5) : to.y; + + let angle; + let x; + let y; + + context.beginPath(); + + angle = Math.atan2(to.y - from.y, to.x - from.x) + x = radius * Math.cos(angle) + xCenter; + y = radius * Math.sin(angle) + yCenter; + + context.moveTo(x, y); + + angle += (1.0 / 3.0) * (2 * Math.PI) + x = radius * Math.cos(angle) + xCenter; + y = radius * Math.sin(angle) + yCenter; + + context.lineTo(x, y); + + angle += (1.0 / 3.0) * (2 * Math.PI) + x = radius * Math.cos(angle) + xCenter; + y = radius * Math.sin(angle) + yCenter; + + context.lineTo(x, y); + + context.closePath(); + + context.fill(); + } + + private roundRect = (context: CanvasRenderingContext2D, x: number, y: number, w: number, h: number, radius: number) => { + + const r = x + w; + const b = y + h; + + context.beginPath(); + context.moveTo(x + radius, y); + context.lineTo(r - radius, y); + context.quadraticCurveTo(r, y, r, y + radius); + context.lineTo(r, y + h - radius); + context.quadraticCurveTo(r, b, r - radius, b); + context.lineTo(x + radius, b); + context.quadraticCurveTo(x, b, x, b - radius); + context.lineTo(x, y + radius); + context.quadraticCurveTo(x, y, x + radius, y); + context.stroke(); + } +} + + diff --git a/slizaa-web/app/src/components/dependencygraph/IDependencyGraphState.ts b/slizaa-web/app/src/components/dependencygraph/IDependencyGraphState.ts new file mode 100644 index 0000000..3c051a7 --- /dev/null +++ b/slizaa-web/app/src/components/dependencygraph/IDependencyGraphState.ts @@ -0,0 +1,23 @@ +/* + * slizaa-web - Slizaa Static Software Analysis Tools + * Copyright © 2019 Code-Kontor GmbH and others (slizaa@codekontor.io) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import {ElkNode} from "elkjs"; + +export interface ISlizaaDependencyListState { + node?: ElkNode +} \ No newline at end of file diff --git a/slizaa-web/app/src/components/dependencygraph/index.tsx b/slizaa-web/app/src/components/dependencygraph/index.tsx new file mode 100644 index 0000000..3d531ba --- /dev/null +++ b/slizaa-web/app/src/components/dependencygraph/index.tsx @@ -0,0 +1,18 @@ +/* + * slizaa-web - Slizaa Static Software Analysis Tools + * Copyright © 2019 Code-Kontor GmbH and others (slizaa@codekontor.io) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +export { DependencyGraph} from './DependencyGraph'; \ No newline at end of file diff --git a/slizaa-web/app/stories/DependencyGraph.stories.tsx b/slizaa-web/app/stories/DependencyGraph.stories.tsx new file mode 100644 index 0000000..ab5c608 --- /dev/null +++ b/slizaa-web/app/stories/DependencyGraph.stories.tsx @@ -0,0 +1,27 @@ +/* + * slizaa-web - Slizaa Static Software Analysis Tools + * Copyright © 2019 Code-Kontor GmbH and others (slizaa@codekontor.io) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import '../src/SlizaaApp.css' + +import { storiesOf } from '@storybook/react'; +import * as React from 'react'; +import {DependencyGraph} from "../src/components/dependencygraph"; + +storiesOf('DependencyGraph', module) + .add('Simple DependencyGraph', () => ( + + )); \ No newline at end of file diff --git a/slizaa-web/app/yarn.lock b/slizaa-web/app/yarn.lock index 97c0aa5..03df287 100644 --- a/slizaa-web/app/yarn.lock +++ b/slizaa-web/app/yarn.lock @@ -3135,6 +3135,11 @@ atob@^2.1.1: resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== +autocompleter@5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/autocompleter/-/autocompleter-5.1.0.tgz#da80488ddf1f1d89b0a8f5d36cab24439de18ab8" + integrity sha512-xFZla6guwywqFJutoi5xrhAmaKw4/TU8CcLuNep/3OtiUfpNXtgzuBkkXJ6ysJIfG6MEEXFtUBg3PREN6HUVyw== + autoprefixer@7.1.6: version "7.1.6" resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-7.1.6.tgz#fb933039f74af74a83e71225ce78d9fd58ba84d7" @@ -6695,6 +6700,11 @@ elegant-spinner@^1.0.1: resolved "https://registry.yarnpkg.com/elegant-spinner/-/elegant-spinner-1.0.1.tgz#db043521c95d7e303fd8f345bedc3349cfb0729e" integrity sha1-2wQ1IcldfjA/2PNFvtwzSc+wcp4= +elkjs@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/elkjs/-/elkjs-0.6.2.tgz#b33ea52cd2e049abf921598e5106995865245bda" + integrity sha512-eAPWONv3c+eT+F1r5dvH/qbyBjPi21LPFlUFaQgB5fCguWTZvp4rjEbVX2iY8TjnprOq9cYXNME38J3Tqxby/w== + elliptic@^6.0.0: version "6.5.0" resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.0.tgz#2b8ed4c891b7de3200e14412a5b8248c7af505ca"