[CORD-814] Rendering nodes and links for coarse tenancy graph
Change-Id: I0a72a667f5a49bb217710cd68b888a5c96ac7995
diff --git a/package.json b/package.json
index fa8fb46..152c4a0 100644
--- a/package.json
+++ b/package.json
@@ -10,6 +10,7 @@
"angular-ui-router": "1.0.0-beta.1",
"bootstrap": "^3.3.7",
"bootstrap-sass": "^3.3.7",
+ "d3": "3.5.17",
"jquery": "^3.1.1",
"lodash": "^4.17.2",
"ngprogress": "^1.1.1",
diff --git a/src/app/service-graph/components/coarse/coarse.component.html b/src/app/service-graph/components/coarse/coarse.component.html
index 75ef18b..c964808 100644
--- a/src/app/service-graph/components/coarse/coarse.component.html
+++ b/src/app/service-graph/components/coarse/coarse.component.html
@@ -1,3 +1,4 @@
<h1>Coarse Tenancy Graph</h1>
-<pre>{{vm.graph | json}}</pre>
\ No newline at end of file
+<svg>
+</svg>
diff --git a/src/app/service-graph/components/coarse/coarse.component.scss b/src/app/service-graph/components/coarse/coarse.component.scss
new file mode 100644
index 0000000..f2497a9
--- /dev/null
+++ b/src/app/service-graph/components/coarse/coarse.component.scss
@@ -0,0 +1,41 @@
+@import './../../../style/vars.scss';
+@import '../../../../../node_modules/bootstrap-sass/assets/stylesheets/bootstrap/variables';
+
+xos-coarse-tenancy-graph {
+ display: block;
+ // background: $color-accent;
+
+ svg {
+ height: 400px;
+ width: 100%;
+ background-color: $panel-filled-bg;
+ border-radius: 3px;
+ }
+
+ .node-group {
+
+ .node {
+ cursor: pointer;
+ }
+
+ .node > rect {
+ stroke: $color-accent;
+ fill: $background-light-color;
+ }
+
+ .node > text {
+ fill: #fff;
+ font-size: 20px;
+ }
+ }
+
+ .link-group {
+ line {
+ stroke: $color-accent;
+ }
+ }
+ .arrow-marker {
+ stroke: $color-accent;
+ fill: $color-accent;
+ }
+}
\ No newline at end of file
diff --git a/src/app/service-graph/components/coarse/coarse.component.ts b/src/app/service-graph/components/coarse/coarse.component.ts
index b64e806..c595cf1 100644
--- a/src/app/service-graph/components/coarse/coarse.component.ts
+++ b/src/app/service-graph/components/coarse/coarse.component.ts
@@ -1,20 +1,166 @@
+import './coarse.component.scss';
+import * as d3 from 'd3';
+import * as $ from 'jquery';
import {IXosServiceGraphStore} from '../../services/graph.store';
-import {IXosServiceGraph} from '../../interfaces';
+import {IXosServiceGraph, IXosServiceGraphNode, IXosServiceGraphLink} from '../../interfaces';
+import {XosServiceGraphConfig as config} from '../../graph.config';
+
class XosCoarseTenancyGraphCtrl {
static $inject = ['$log', 'XosServiceGraphStore'];
public graph: IXosServiceGraph;
+ private svg;
+ private forceLayout;
+ private linkGroup;
+ private nodeGroup;
+
constructor (
private $log: ng.ILogService,
private XosServiceGraphStore: IXosServiceGraphStore
) {
this.XosServiceGraphStore.getCoarse()
- .subscribe((res) => {
+ .subscribe((res: IXosServiceGraph) => {
+ // id there are no data, do nothing
+ if (!res.nodes || res.nodes.length === 0 || !res.links || res.links.length === 0) {
+ return;
+ }
this.graph = res;
+ this.setupForceLayout(res);
+ this.renderNodes(res.nodes);
+ this.renderLinks(res.links);
+ this.forceLayout.start();
});
+
+ this.handleSvg();
+ }
+
+ private getSvgDimensions(): {width: number, heigth: number} {
+ return {
+ width: $('xos-coarse-tenancy-graph svg').width(),
+ heigth: $('xos-coarse-tenancy-graph svg').height()
+ };
+ }
+
+ private handleSvg() {
+ this.svg = d3.select('svg');
+
+ this.svg.append('svg:defs')
+ .selectAll('marker')
+ .data(config.markers)
+ .enter()
+ .append('svg:marker')
+ .attr('id', d => d.id)
+ .attr('viewBox', d => d.viewBox)
+ .attr('refX', d => d.refX)
+ .attr('refY', d => d.refY)
+ .attr('markerWidth', d => d.width)
+ .attr('markerHeight', d => d.height)
+ .attr('orient', 'auto')
+ .attr('class', d => `${d.id}-marker`)
+ .append('svg:path')
+ .attr('d', d => d.path);
+
+ this.linkGroup = this.svg.append('g')
+ .attr({
+ class: 'link-group'
+ });
+
+ this.nodeGroup = this.svg.append('g')
+ .attr({
+ class: 'node-group'
+ });
+ }
+
+ private setupForceLayout(data: IXosServiceGraph) {
+
+ const tick = () => {
+ this.nodeGroup.selectAll('g.node')
+ .attr({
+ transform: d => `translate(${d.x}, ${d.y})`
+ });
+
+ this.linkGroup.selectAll('line')
+ .attr({
+ x1: l => l.source.x,
+ y1: l => l.source.y,
+ x2: l => l.target.x,
+ y2: l => l.target.y,
+ });
+ };
+ const svgDim = this.getSvgDimensions();
+ this.forceLayout = d3.layout.force()
+ .size([svgDim.width, svgDim.heigth])
+ .nodes(data.nodes)
+ .links(data.links)
+ .linkDistance(config.force.linkDistance)
+ .charge(config.force.charge)
+ .gravity(config.force.gravity)
+ .on('tick', tick);
+ }
+
+ private getSiblingTextBBox(contex: any /* D3 this */) {
+ return d3.select(contex.parentNode).select('text').node().getBBox();
+ }
+
+ private renderNodes(nodes: IXosServiceGraphNode[]) {
+ const self = this;
+ const node = this.nodeGroup
+ .selectAll('rect')
+ .data(nodes);
+
+ const entering = node.enter()
+ .append('g')
+ .attr({
+ class: 'node',
+ })
+ .call(this.forceLayout.drag)
+ .on('mousedown', () => { d3.event.stopPropagation(); })
+ .on('mouseup', (d) => { d.fixed = true; });
+
+ entering.append('rect')
+ .attr({
+ rx: config.node.radius,
+ ry: config.node.radius
+ });
+
+ entering.append('text')
+ .attr({
+ 'text-anchor': 'middle'
+ })
+ .text(n => n.label);
+
+ const existing = node.selectAll('rect');
+
+
+ // resize node > rect as contained text
+ existing.each(function() {
+ const textBBox = self.getSiblingTextBBox(this);
+ const rect = d3.select(this);
+ rect.attr({
+ width: textBBox.width + config.node.padding,
+ height: textBBox.height + config.node.padding,
+ x: textBBox.x - (config.node.padding / 2),
+ y: textBBox.y - (config.node.padding / 2)
+ });
+ });
+ }
+
+ private renderLinks(links: IXosServiceGraphLink[]) {
+ const link = this.linkGroup
+ .selectAll('rect')
+ .data(links);
+
+ const entering = link.enter()
+ .append('g')
+ .attr({
+ class: 'link',
+ });
+
+ entering.append('line')
+ .attr('marker-start', 'url(#arrow)');
}
}
diff --git a/src/app/service-graph/graph.config.ts b/src/app/service-graph/graph.config.ts
new file mode 100644
index 0000000..e94e510
--- /dev/null
+++ b/src/app/service-graph/graph.config.ts
@@ -0,0 +1,45 @@
+interface ISvgMarker {
+ id: string;
+ width: number;
+ height: number;
+ refX: number;
+ refY: number;
+ viewBox: string;
+ path: string; // svg path
+}
+
+export interface IXosServiceGraphConfig {
+ force: {
+ linkDistance: number;
+ charge: number;
+ gravity: number;
+ };
+ node: {
+ padding: number;
+ radius: number;
+ };
+ markers: ISvgMarker[];
+}
+
+export const XosServiceGraphConfig: IXosServiceGraphConfig = {
+ force: {
+ linkDistance: 160,
+ charge: -60,
+ gravity: 0.01
+ },
+ node: {
+ padding: 10,
+ radius: 2
+ },
+ markers: [
+ {
+ id: 'arrow',
+ width: 10,
+ height: 10,
+ refX: -80,
+ refY: 0,
+ viewBox: '0 -5 10 10',
+ path: 'M10,-5L0,0L10,5'
+ }
+ ]
+};
diff --git a/src/app/service-graph/services/graph.store.ts b/src/app/service-graph/services/graph.store.ts
index f9d4d15..9636bb2 100644
--- a/src/app/service-graph/services/graph.store.ts
+++ b/src/app/service-graph/services/graph.store.ts
@@ -118,21 +118,25 @@
});
}
+ private getNodeIndexById(id: number, nodes: IXosServiceModel[]) {
+ return _.findIndex(nodes, {id: id});
+ }
+
private graphDataToCoarseGraph(data: IXosCoarseGraphData) {
+ // TODO find how to bind source/target by node ID and not by position in array (ask Simon?)
const links: IXosServiceGraphLink[] = _.chain(data.tenants)
.filter((t: IXosTenantModel) => t.kind === 'coarse')
.map((t: IXosTenantModel) => {
return {
id: t.id,
- source: t.provider_service_id,
- target: t.subscriber_service_id,
+ source: this.getNodeIndexById(t.provider_service_id, data.services),
+ target: this.getNodeIndexById(t.subscriber_service_id, data.services),
model: t
};
})
.value();
- // NOTE show all services anyway or find only the node that have links pointing to it
const nodes: IXosServiceGraphNode[] = _.map(data.services, (s: IXosServiceModel) => {
return {
id: s.id,
@@ -141,7 +145,6 @@
};
});
- // TODO call next on this.d3CoarseGraph
this.d3CoarseGraph.next({
nodes: nodes,
links: links
diff --git a/src/app/style/style.scss b/src/app/style/style.scss
index 3151e6c..198c712 100644
--- a/src/app/style/style.scss
+++ b/src/app/style/style.scss
@@ -1301,7 +1301,7 @@
}
.panel.panel-filled{
-background-color:rgba(68,70,79,0.5)
+background-color: $panel-filled-bg;
}
.view-header{
diff --git a/src/app/style/vars.scss b/src/app/style/vars.scss
index 3c7ce77..05aeeea 100644
--- a/src/app/style/vars.scss
+++ b/src/app/style/vars.scss
@@ -15,6 +15,8 @@
$border-color: lighten($background-color, 6%);
$color-placeholder: darken($color-font, 14%);
+$panel-filled-bg: rgba(68,70,79,0.5);
+
$color-primary: #0F83C9;
$color-info: #56C0E0;
$color-warning: #f7af3e;
diff --git a/src/app/views/dashboard/dashboard.html b/src/app/views/dashboard/dashboard.html
index 91481ad..5d4e89a 100644
--- a/src/app/views/dashboard/dashboard.html
+++ b/src/app/views/dashboard/dashboard.html
@@ -8,6 +8,9 @@
<div class="col-xs-12" id="dashboard-component-container"></div>
</div>
<div class="row">
+ <div class="col-xs-12">
+ <h2>System summary:</h2>
+ </div>
<div class="col-xs-4">
<div class="panel panel-filled">
<div class="panel-body text-center">