Skip to content

Commit

Permalink
#20 First draft dependency graph component
Browse files Browse the repository at this point in the history
  • Loading branch information
wuetherich committed Mar 25, 2020
1 parent c1e2f50 commit 3e3dbf8
Show file tree
Hide file tree
Showing 6 changed files with 336 additions and 0 deletions.
1 change: 1 addition & 0 deletions slizaa-web/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
257 changes: 257 additions & 0 deletions slizaa-web/app/src/components/dependencygraph/DependencyGraph.tsx
Original file line number Diff line number Diff line change
@@ -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<any, ISlizaaDependencyListState> {

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 <div id="dsm-canvas-container">
<canvas id="main" ref={ref => (this.canvasRef = ref)}/>
</div>
}

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();
}
}


Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* slizaa-web - Slizaa Static Software Analysis Tools
* Copyright © 2019 Code-Kontor GmbH and others ([email protected])
*
* 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 <http://www.gnu.org/licenses/>.
*/

import {ElkNode} from "elkjs";

export interface ISlizaaDependencyListState {
node?: ElkNode
}
18 changes: 18 additions & 0 deletions slizaa-web/app/src/components/dependencygraph/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* slizaa-web - Slizaa Static Software Analysis Tools
* Copyright © 2019 Code-Kontor GmbH and others ([email protected])
*
* 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 <http://www.gnu.org/licenses/>.
*/
export { DependencyGraph} from './DependencyGraph';
27 changes: 27 additions & 0 deletions slizaa-web/app/stories/DependencyGraph.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* slizaa-web - Slizaa Static Software Analysis Tools
* Copyright © 2019 Code-Kontor GmbH and others ([email protected])
*
* 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 <http://www.gnu.org/licenses/>.
*/
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', () => (
<DependencyGraph />
));
10 changes: 10 additions & 0 deletions slizaa-web/app/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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==

[email protected]:
version "5.1.0"
resolved "https://registry.yarnpkg.com/autocompleter/-/autocompleter-5.1.0.tgz#da80488ddf1f1d89b0a8f5d36cab24439de18ab8"
integrity sha512-xFZla6guwywqFJutoi5xrhAmaKw4/TU8CcLuNep/3OtiUfpNXtgzuBkkXJ6ysJIfG6MEEXFtUBg3PREN6HUVyw==

[email protected]:
version "7.1.6"
resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-7.1.6.tgz#fb933039f74af74a83e71225ce78d9fd58ba84d7"
Expand Down Expand Up @@ -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"
Expand Down

0 comments on commit 3e3dbf8

Please sign in to comment.