[CORD-814] Rendering nodes and links for coarse tenancy graph

Change-Id: I0a72a667f5a49bb217710cd68b888a5c96ac7995
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">