Drawing subscriber and devices
diff --git a/views/ngXosViews/diagnostic/src/js/serviceTopologyHelper.js b/views/ngXosViews/diagnostic/src/js/serviceTopologyHelper.js
new file mode 100644
index 0000000..f3cb2d2
--- /dev/null
+++ b/views/ngXosViews/diagnostic/src/js/serviceTopologyHelper.js
@@ -0,0 +1,258 @@
+(function () {
+  'use strict';
+
+  angular.module('xos.serviceTopology')
+  .service('ServiceTopologyHelper', function($window, $log, lodash, ServiceRelation, serviceTopologyConfig){
+
+    const drawLegend = (svg) => {
+      const legendContainer = svg.append('g')
+        .attr({
+          class: 'legend'
+        });
+
+      legendContainer.append('rect')
+      .attr({
+        transform: d => `translate(10, 80)`,
+        width: 100,
+        height: 100
+      });
+
+      // service
+      const service = legendContainer.append('g')
+      .attr({
+        class: 'node service'
+      });
+
+      service.append('circle')
+      .attr({
+        r: serviceTopologyConfig.circle.radius,
+        transform: d => `translate(30, 100)`
+      });
+
+      service.append('text')
+      .attr({
+        transform: d => `translate(45, 100)`,
+        dy: '.35em'
+      })
+      .text('Service')
+        .style('fill-opacity', 1);
+
+      // slice
+      const slice = legendContainer.append('g')
+        .attr({
+          class: 'node slice'
+        });
+
+      slice.append('rect')
+        .attr({
+          width: 20,
+          height: 20,
+          x: -10,
+          y: -10,
+          transform: d => `translate(30, 130)`
+        });
+
+      slice.append('text')
+        .attr({
+          transform: d => `translate(45, 130)`,
+          dy: '.35em'
+        })
+        .text('Slices')
+        .style('fill-opacity', 1);
+
+      // instance
+      const instance = legendContainer.append('g')
+        .attr({
+          class: 'node instance'
+        });
+
+      instance.append('rect')
+        .attr({
+          width: 20,
+          height: 20,
+          x: -10,
+          y: -10,
+          transform: d => `translate(30, 160)`
+        });
+
+      instance.append('text')
+        .attr({
+          transform: d => `translate(45, 160)`,
+          dy: '.35em'
+        })
+        .text('Instances')
+        .style('fill-opacity', 1);
+    };
+
+    var _svg, _layout, _source;
+
+    var i = 0;
+
+    // given a canvas, a layout and a data source, draw a tree layout
+    const updateTree = (svg, layout, source) => {
+
+      //cache data
+      _svg = svg;
+      _layout = layout;
+      _source = source;
+
+      const maxDepth = ServiceRelation.depthOf(source);
+
+      const diagonal = d3.svg.diagonal()
+        .projection(d => [d.y, d.x]);
+
+      // Compute the new tree layout.
+      var nodes = layout.nodes(source).reverse(),
+        links = layout.links(nodes);
+
+      // Normalize for fixed-depth.
+      nodes.forEach(function(d) {
+        // position the child node horizontally
+        const step = (($window.innerWidth - (serviceTopologyConfig.widthMargin * 2)) / maxDepth);
+        d.y = d.depth * step;
+      });
+
+      // Update the nodes…
+      var node = svg.selectAll('g.node')
+        .data(nodes, function(d) { return d.id || (d.id = ++i); });
+
+      // Enter any new nodes at the parent's previous position.
+      var nodeEnter = node.enter().append('g')
+        .attr({
+          class: d => `node ${d.type}`,
+          transform: `translate(${source.y0}, ${source.x0})`
+        });
+
+      const subscriberNodes = nodeEnter.filter('.subscriber');
+      const internetNodes = nodeEnter.filter('.internet');
+      const serviceNodes = nodeEnter.filter('.service');
+      const instanceNodes = nodeEnter.filter('.instance');
+      const sliceNodes = nodeEnter.filter('.slice');
+
+      subscriberNodes.append('rect')
+        .attr(serviceTopologyConfig.square);
+
+      internetNodes.append('rect')
+        .attr(serviceTopologyConfig.square);
+
+      serviceNodes.append('circle')
+        .attr('r', 1e-6)
+        .style('fill', d => d._children ? 'lightsteelblue' : '#fff')
+        .on('click', serviceClick);
+
+      sliceNodes.append('rect')
+        .attr({
+          width: 20,
+          height: 20,
+          x: -10,
+          y: -10
+        });
+
+      instanceNodes.append('rect')
+        .attr({
+          width: 20,
+          height: 20,
+          x: -10,
+          y: -10,
+          class: d => d.active ?'' : 'active'
+        });
+
+      nodeEnter.append('text')
+        .attr({
+          x: d => d.children ? -serviceTopologyConfig.circle.selectedRadius -3 : serviceTopologyConfig.circle.selectedRadius + 3,
+          dy: '.35em',
+          transform: d => {
+            if (d.children && d.parent){
+              if(d.parent.x < d.x){
+                return 'rotate(-30)';
+              }
+              return 'rotate(30)';
+            }
+          },
+          'text-anchor': d => d.children ? 'end' : 'start'
+        })
+        .text(d => d.name)
+        .style('fill-opacity', 1e-6);
+
+      // Transition nodes to their new position.
+      var nodeUpdate = node.transition()
+        .duration(serviceTopologyConfig.duration)
+        .attr({
+          'transform': d => `translate(${d.y},${d.x})`
+        });
+
+      nodeUpdate.select('circle')
+        .attr('r', d => d.selected ? serviceTopologyConfig.circle.selectedRadius : serviceTopologyConfig.circle.radius)
+        .style('fill', d => d.selected ? 'lightsteelblue' : '#fff');
+
+      nodeUpdate.select('text')
+        .style('fill-opacity', 1);
+
+      // Transition exiting nodes to the parent's new position.
+      var nodeExit = node.exit().transition()
+        .duration(serviceTopologyConfig.duration)
+        .remove();
+
+      nodeExit.select('circle')
+        .attr('r', 1e-6);
+
+      nodeExit.select('text')
+        .style('fill-opacity', 1e-6);
+
+      // Update the links…
+      var link = svg.selectAll('path.link')
+        .data(links, function(d) { return d.target.id; });
+
+      // Enter any new links at the parent's previous position.
+      link.enter().insert('path', 'g')
+        .attr('class', d => `link ${d.target.type} ${d.target.active ? '' : 'active'}`)
+        .attr('d', function(d) {
+          var o = {x: source.x0, y: source.y0};
+          return diagonal({source: o, target: o});
+        });
+
+      // Transition links to their new position.
+      link.transition()
+        .duration(serviceTopologyConfig.duration)
+        .attr('d', diagonal);
+
+      // Transition exiting nodes to the parent's new position.
+      link.exit().transition()
+        .duration(serviceTopologyConfig.duration)
+        .attr('d', function(d) {
+          var o = {x: source.x, y: source.y};
+          return diagonal({source: o, target: o});
+        })
+        .remove();
+
+      // Stash the old positions for transition.
+      nodes.forEach(function(d) {
+        d.x0 = d.x;
+        d.y0 = d.y;
+      });
+    };
+
+    const serviceClick = function(d) {
+
+      $log.info('TODO emit an event to highlight VMs');
+
+      if(!d.service){
+        return;
+      }
+
+      // toggling selected status
+      d.selected = !d.selected;
+
+      var selectedNode = d3.select(this);
+
+      selectedNode
+        .transition()
+        .duration(serviceTopologyConfig.duration)
+        .attr('r', serviceTopologyConfig.circle.selectedRadius);
+    };
+
+    this.updateTree = updateTree;
+    this.drawLegend = drawLegend;
+  });
+
+}());
\ No newline at end of file