Added GUI to intent perf app to monitor performance stats in real-time.

Fixed app ids for metrics app.

Change-Id: Icea99991ad71c80c53a832c236dcc05fefbb9b02
diff --git a/apps/test/intent-perf/src/main/java/org/onosproject/intentperf/IntentPerfCollector.java b/apps/test/intent-perf/src/main/java/org/onosproject/intentperf/IntentPerfCollector.java
index 90eb9d7..8683f3f 100644
--- a/apps/test/intent-perf/src/main/java/org/onosproject/intentperf/IntentPerfCollector.java
+++ b/apps/test/intent-perf/src/main/java/org/onosproject/intentperf/IntentPerfCollector.java
@@ -65,7 +65,7 @@
     @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
     protected ClusterService clusterService;
 
-    @Reference(cardinality = ReferenceCardinality.OPTIONAL_UNARY)
+    @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
     protected IntentPerfUi ui;
 
     // Auxiliary structures used to accrue data for normalized time interval
@@ -99,6 +99,7 @@
             nodeToIndex.put(nodes[i].id(), i);
         }
 
+        ui.setHeaders(getSampleHeaders());
         clearSamples();
         log.info("Started");
     }
diff --git a/apps/test/intent-perf/src/main/java/org/onosproject/intentperf/IntentPerfInstaller.java b/apps/test/intent-perf/src/main/java/org/onosproject/intentperf/IntentPerfInstaller.java
index 02aa1b3..96f2b32 100644
--- a/apps/test/intent-perf/src/main/java/org/onosproject/intentperf/IntentPerfInstaller.java
+++ b/apps/test/intent-perf/src/main/java/org/onosproject/intentperf/IntentPerfInstaller.java
@@ -99,7 +99,7 @@
     private static final int DEFAULT_NUM_NEIGHBORS = 0;
 
     private static final int START_DELAY = 5_000; // ms
-    private static final int REPORT_PERIOD = 5_000; //ms
+    private static final int REPORT_PERIOD = 1_000; //ms
 
     private static final String START = "start";
     private static final String STOP = "stop";
diff --git a/apps/test/intent-perf/src/main/java/org/onosproject/intentperf/IntentPerfUi.java b/apps/test/intent-perf/src/main/java/org/onosproject/intentperf/IntentPerfUi.java
index d30fe4e..f50ed3b 100644
--- a/apps/test/intent-perf/src/main/java/org/onosproject/intentperf/IntentPerfUi.java
+++ b/apps/test/intent-perf/src/main/java/org/onosproject/intentperf/IntentPerfUi.java
@@ -15,6 +15,7 @@
  */
 package org.onosproject.intentperf;
 
+import com.fasterxml.jackson.databind.node.ArrayNode;
 import com.fasterxml.jackson.databind.node.ObjectNode;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
@@ -23,6 +24,7 @@
 import org.apache.felix.scr.annotations.Deactivate;
 import org.apache.felix.scr.annotations.Reference;
 import org.apache.felix.scr.annotations.ReferenceCardinality;
+import org.apache.felix.scr.annotations.Service;
 import org.onlab.osgi.ServiceDirectory;
 import org.onosproject.intentperf.IntentPerfCollector.Sample;
 import org.onosproject.ui.UiConnection;
@@ -34,14 +36,17 @@
 import java.util.Collection;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Random;
 import java.util.Set;
+import java.util.TimerTask;
 
 import static java.util.Collections.synchronizedSet;
 
 /**
  * Mechanism to stream data to the GUI.
  */
-@Component(immediate = true, enabled = false)
+@Component(immediate = true, enabled = true)
+@Service(value = IntentPerfUi.class)
 public class IntentPerfUi {
 
     @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
@@ -53,9 +58,25 @@
     private UiExtension uiExtension = new UiExtension(views, this::newHandlers,
                                                       getClass().getClassLoader());
 
+    private List<String> headers = ImmutableList.of("One", "Two", "Three", "Four", "Five");
+
+    private Random random = new Random();
+    private TimerTask task;
+
     @Activate
     protected void activate() {
         uiExtensionService.register(uiExtension);
+//        task = new TimerTask() {
+//            @Override
+//            public void run() {
+//                Sample sample = new Sample(System.currentTimeMillis(), headers.size());
+//                for (int i = 0; i < headers.size(); i++) {
+//                    sample.data[i] = 25_000 + random.nextInt(20_000) - 5_000;
+//                }
+//                reportSample(sample);
+//            }
+//        };
+//        SharedExecutors.getTimer().scheduleAtFixedRate(task, 1000, 1000);
     }
 
     @Deactivate
@@ -74,6 +95,15 @@
         }
     }
 
+    /**
+     * Sets the headers for the subsequently reported samples.
+     *
+     * @param headers list of headers for future samples
+     */
+    public void setHeaders(List<String> headers) {
+        this.headers = headers;
+    }
+
     // Creates and returns session specific message handler.
     private Collection<UiMessageHandler> newHandlers() {
         return ImmutableList.of(new StreamingControl());
@@ -90,7 +120,22 @@
 
         @Override
         public void process(ObjectNode message) {
-            streamingEnabled = message.path("event").asText("unknown").equals("initPerfStart");
+            streamingEnabled = message.path("event").asText("unknown").equals("intentPerfStart");
+            if (streamingEnabled) {
+                sendHeaders();
+            }
+        }
+
+        private void sendHeaders() {
+            ArrayNode an = mapper.createArrayNode();
+            for (String header : headers) {
+                an.add(header);
+            }
+
+            ObjectNode sn = mapper.createObjectNode();
+            sn.set("headers", an);
+
+            connection().sendMessage("intentPerfHeaders", 0, sn);
         }
 
         @Override
@@ -106,10 +151,18 @@
         }
 
         private void send(Sample sample) {
-            // FIXME: finish this
-            ObjectNode sn = mapper.createObjectNode()
-                    .put("time", sample.time);
-            connection().sendMessage("intentPerf", 0, sn);
+            if (streamingEnabled) {
+                ArrayNode an = mapper.createArrayNode();
+                for (double d : sample.data) {
+                    an.add(d);
+                }
+
+                ObjectNode sn = mapper.createObjectNode();
+                sn.put("time", sample.time);
+                sn.set("data", an);
+
+                connection().sendMessage("intentPerfSample", 0, sn);
+            }
         }
     }
 
diff --git a/apps/test/intent-perf/src/main/resources/app/view/intentPerf/intentPerf.css b/apps/test/intent-perf/src/main/resources/app/view/intentPerf/intentPerf.css
index 604a169..d98b620 100644
--- a/apps/test/intent-perf/src/main/resources/app/view/intentPerf/intentPerf.css
+++ b/apps/test/intent-perf/src/main/resources/app/view/intentPerf/intentPerf.css
@@ -18,39 +18,35 @@
  ONOS GUI -- Intent Perf View -- CSS file
  */
 
-.light #ov-intentPerf {
-    color: navy;
+svg {
+    font: 12px sans-serif;
 }
 
-.dark #ov-intentPerf {
-    color: #1e5e6f;
+.line {
+    fill: none;
+    stroke: #000;
+    stroke-width: 2px;
 }
 
-.dark a {
-    color: #88c;
-}
-
-#ov-intentPerf .msg {
-    color: darkorange;
-}
-
-.light #ov-intentPerf .msg {
-    color: darkorange;
-}
-
-.dark #ov-intentPerf .msg {
-    color: #904e00;
-}
-
-
-
 .axis path,
 .axis line {
     fill: none;
-    stroke: #000;
+    stroke-width: 2px;
     shape-rendering: crispEdges;
 }
 
-.browser text {
-    text-anchor: end;
+.light .axis path,
+.light .axis line,
+.light .axis text {
+    stroke: #999;
 }
+
+.dark .axis path,
+.dark .axis line,
+.dark .axis text {
+    stroke: #eee;
+}
+
+.axis text {
+    stroke-width: 0.3;
+}
\ No newline at end of file
diff --git a/apps/test/intent-perf/src/main/resources/app/view/intentPerf/intentPerf.html b/apps/test/intent-perf/src/main/resources/app/view/intentPerf/intentPerf.html
index b1ef9d2..6cdbc66 100644
--- a/apps/test/intent-perf/src/main/resources/app/view/intentPerf/intentPerf.html
+++ b/apps/test/intent-perf/src/main/resources/app/view/intentPerf/intentPerf.html
@@ -15,10 +15,12 @@
   -->
 
 <!-- Intent Performance partial HTML -->
-<div id="ov-sample">
+<div id="ov-intentPerf">
     <h2> Intent Performance View </h2>
 
-    <span class="msg">{{ ctrl.message }}</span>
-
-    <div id="intent-perf-chart"></div>
+    <div id="intent-perf-chart"
+         resize
+         ng-style="resizeWithOffset(56, 12)"
+         notifier="ctrl.notifyResize()">
+    </div>
 </div>
diff --git a/apps/test/intent-perf/src/main/resources/app/view/intentPerf/intentPerf.js b/apps/test/intent-perf/src/main/resources/app/view/intentPerf/intentPerf.js
index e18c47d..4fcdb0a 100644
--- a/apps/test/intent-perf/src/main/resources/app/view/intentPerf/intentPerf.js
+++ b/apps/test/intent-perf/src/main/resources/app/view/intentPerf/intentPerf.js
@@ -21,122 +21,248 @@
     'use strict';
 
     // injected refs
-    var $log, tbs, flash;
+    var $log, tbs, ts, wss, sus, flash, fs, mast;
 
-    function start() {
-        //var format = d3.time.format("%m/%d/%y");
-        var format = d3.time.format("%H:%M:%S");
-        var samples = [];
+    // internal state
+    var handlerMap,
+        openListener,
+        theSample = [],
+        graph;
 
-        var margin = {top: 20, right: 30, bottom: 30, left: 40},
-            width = 960 - margin.left - margin.right,
-            height = 500 - margin.top - margin.bottom;
+    // ==========================
 
-        var x = d3.time.scale()
-            .range([0, width]);
+    function createGraph(h) {
+        var stopped = false,
+            n = 243,
+            duration = 750,
+            now = new Date(Date.now() - duration),
+            headers = h,
+            data = [];
 
-        var y = d3.scale.linear()
-            .range([height, 0]);
+        var dim = fs.windowSize(mast.mastHeight());
+        var margin, width, height, x, y;
+        var svg, axis;
 
-        var z = d3.scale.category20c();
+        var lines = [],
+            paths = [];
 
-        var xAxis = d3.svg.axis()
-            .scale(x)
-            .orient("bottom")
-            .ticks(d3.time.seconds);
+        var transition = d3.select({}).transition()
+            .duration(duration)
+            .ease("linear");
 
-        var yAxis = d3.svg.axis()
-            .scale(y)
-            .orient("left");
+        svg = d3.select("#intent-perf-chart").append("p")
+            .append("svg").attr("id", "intent-perf-svg")
+            .append("g").attr("id", "intent-perf-svg-g");
 
-        var stack = d3.layout.stack()
-            .offset("zero")
-            .values(function(d) { return d.values; })
-            .x(function(d) { return d.date; })
-            .y(function(d) { return d.value; });
+        svg.append("defs").append("clipPath").attr("id", "intent-perf-clip")
+            .append("rect");
 
-        var nest = d3.nest()
-            .key(function(d) { return d.key; });
-
-        var area = d3.svg.area()
-            .interpolate("cardinal")
-            .x(function(d) { return x(d.date); })
-            .y0(function(d) { return y(d.y0); })
-            .y1(function(d) { return y(d.y0 + d.y); });
-
-        var svg = d3.select("body").append("svg")
-            .attr("width", width + margin.left + margin.right)
-            .attr("height", height + margin.top + margin.bottom)
-            .append("g")
-            .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
-
-        svg.append("g")
+        axis = svg.append("g")
             .attr("class", "x axis")
-            .attr("transform", "translate(0," + height + ")")
-            .call(xAxis);
+            .attr("id", "intent-perf-x");
+
+        svg.append("g").attr("class", "y axis")
+            .attr("id", "intent-perf-yl");
 
         svg.append("g")
             .attr("class", "y axis")
-            .call(yAxis);
+            .attr("id", "intent-perf-yr");
 
-        function fetchData() {
-            d3.csv("app/view/intentPerf/data.csv", function (data) {
-                samples = data;
-                updateGraph();
-            });
+        resize(dim);
+
+        headers.forEach(function (h, li) {
+            // Prime the data to match the headeres and zero it out.
+            data[li] = d3.range(n).map(function() { return 0 });
+            theSample[li] = 0;
+
+            // Create the lines
+            lines[li] = d3.svg.line()
+                .interpolate("basis")
+                .x(function(d, i) { return x(now - (n - 1 - i) * duration); })
+                .y(function(d, i) { return y(d); });
+
+            // Create the SVG paths
+            paths[li] = svg.append("g")
+                .attr("clip-path", "url(#intent-perf-clip)")
+                .append("path")
+                .datum(function () { return data[li]; })
+                .attr("id", "line" + li)
+                .style("stroke", lineColor(li))
+                .attr("class", "line");
+        });
+
+        function lineColor(li) {
+            return li < headers.length - 1 ?
+                sus.cat7().getColor(li, false, ts.theme()) :
+                ts.theme() == 'light' ? '#333' : '#eee';
         }
 
-        function updateGraph() {
-            samples.forEach(function(d) {
-                d.date = format.parse(d.date);
-                d.value = +d.value;
+        function tick() {
+            if (stopped) {
+                return;
+            }
+
+            transition = transition.each(function() {
+                // update the domains
+                now = new Date();
+                x.domain([now - (n - 2) * duration, now - duration]);
+
+                data.forEach(function (d, li) {
+                    // push the new most recent sample onto the back
+                    d.push(theSample[li]);
+
+                    // redraw the line and slide it left
+                    paths[li].attr("d", lines[li]).attr("transform", null);
+                    paths[li].transition().attr("transform", "translate(" + x(now - (n - 1) * duration) + ")");
+
+                    // pop the old data point off the front
+                    d.shift();
+                });
+
+                // slide the x-axis left
+                axis.call(x.axis);
+            }).transition().each("start", tick);
+        }
+
+        function start() {
+            stopped = false;
+            headers.forEach(function (h, li) {
+                theSample[li] = 0;
             });
+            tick();
+        }
 
-            var layers = stack(nest.entries(samples));
+        function stop() {
+            stopped = true;
+        }
 
-            x.domain(d3.extent(samples, function(d) { return d.date; }));
-            y.domain([0, d3.max(samples, function(d) { return d.y0 + d.y; })]);
+        function resize(dim) {
+            margin = {top: 20, right: 90, bottom: 20, left: 70};
+            width = dim.width - margin.right - margin.left;
+            height = 480 - margin.top - margin.bottom;
 
-            svg.selectAll(".layer")
-                .data(layers)
-                .enter().append("path")
-                .attr("class", "layer")
-                .attr("d", function(d) { return area(d.values); })
-                .style("fill", function(d, i) { return z(i); });
+            x = d3.time.scale()
+                .domain([now - (n - 2) * duration, now - duration])
+                .range([0, width]);
 
-            svg.select(".x")
+            y = d3.scale.linear()
+                .domain([0, 200000])
+                .range([height, 0]);
+
+            d3.select("#intent-perf-svg")
+                .attr("width", width + margin.left + margin.right)
+                .attr("height", height + margin.top + margin.bottom);
+            d3.select("#intent-perf-svg-g")
+                .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
+
+            d3.select("#intent-pef-clip rect").attr("width", width).attr("height", height);
+
+            d3.select("#intent-perf-x")
                 .attr("transform", "translate(0," + height + ")")
-                .call(xAxis);
+                .call(x.axis = d3.svg.axis().scale(x).orient("bottom"));
 
-            svg.select(".y")
-                .call(yAxis);
-
-            console.log('tick');
+            d3.select("#intent-perf-yl")
+                .call(d3.svg.axis().scale(y).orient("left"))
+            d3.select("#intent-perf-yr")
+                .attr("transform", "translate(" + width + " ,0)")
+                .call(d3.svg.axis().scale(y).orient("right"))
         }
+
+        return {
+            start: start,
+            stop: stop,
+            resize: resize
+        };
     }
 
-    start();
+
+    function wsOpen(host, url) {
+        $log.debug('IntentPerf: web socket open - cluster node:', host, 'URL:', url);
+        // Request batch of initial data from the new server
+        wss.sendEvent('intentPerfStart');
+    }
+
+    function createAndInitGraph(d) {
+        if (!graph) {
+            d.headers.push("total");
+            graph = createGraph(d.headers);
+        }
+        graph.start();
+    }
+
+    function graphResized(dim) {
+        $log.info("Resized: " + dim.width + "x" + dim.height);
+        graph.resize(dim);
+    }
+
+    function recordSample(sample) {
+        var total = 0;
+        sample.data.forEach(function (d, i) {
+            theSample[i] = d;
+            total = total + d;
+        });
+        theSample[sample.data.length] = total;
+    }
+
+    function createHandlerMap() {
+        handlerMap = {
+            intentPerfHeaders: createAndInitGraph,
+            intentPerfSample: recordSample
+        };
+    }
+
+    //setInterval(function () { theSample = samples[++cs]; }, 5000);
+    //createGraph();
 
     // define the controller
 
     angular.module('ovIntentPerf', ['onosUtil'])
     .controller('OvIntentPerfCtrl',
-        ['$scope', '$log', 'ToolbarService', 'FlashService',
+        ['$scope', '$log', 'ToolbarService', 'WebSocketService',
+            'ThemeService', 'FlashService', 'SvgUtilService', 'FnService',
+            'MastService',
 
-        function ($scope, _$log_, _tbs_, _flash_) {
-            var self = this
+        function ($scope, _$log_, _tbs_, _wss_, _ts_, _flash_, _sus_, _fs_, _mast_) {
+            var self = this;
 
             $log = _$log_;
             tbs = _tbs_;
+            wss = _wss_;
+            ts = _ts_;
             flash = _flash_;
+            sus = _sus_;
+            fs = _fs_;
+            mast = _mast_;
 
-            self.message = 'Hey there dudes!';
-            start();
+            createHandlerMap();
 
-            // Clean up on destroyed scope
+            self.notifyResize = function () {
+                graphResized(fs.windowSize(mast.mastHeight()));
+            };
+
+            function start() {
+                openListener = wss.addOpenListener(wsOpen);
+                wss.bindHandlers(handlerMap);
+                wss.sendEvent('intentPerfStart');
+                $log.debug('intentPerf comms started');
+            }
+
+            function stop() {
+                graph.stop();
+                wss.sendEvent('intentPerfStop');
+                wss.unbindHandlers(handlerMap);
+                wss.removeOpenListener(openListener);
+                openListener = null;
+                graph = null;
+                $log.debug('intentPerf comms stopped');
+            }
+
+            // Cleanup on destroyed scope..
             $scope.$on('$destroy', function () {
+                $log.log('OvIntentPerfCtrl is saying Buh-Bye!');
+                stop();
             });
 
-         $log.log('OvIntentPerfCtrl has been created');
-    }]);
+            start();
+        }]);
 }());
diff --git a/apps/test/intent-perf/src/main/resources/dev.html b/apps/test/intent-perf/src/main/resources/dev.html
index ad059df..d0b59f4 100644
--- a/apps/test/intent-perf/src/main/resources/dev.html
+++ b/apps/test/intent-perf/src/main/resources/dev.html
@@ -18,8 +18,6 @@
 <head>
     <title>Dev View</title>
     <script src="tp/d3.min.js"></script>
-    <script src="tp/jquery-2.1.1.min.js"></script>
-
     <link rel="stylesheet" href="app/view/intentPerf/intentPerf.css">
 </head>
 <body>
diff --git a/apps/test/intent-perf/src/main/resources/sliding.html b/apps/test/intent-perf/src/main/resources/sliding.html
new file mode 100644
index 0000000..8dc501b
--- /dev/null
+++ b/apps/test/intent-perf/src/main/resources/sliding.html
@@ -0,0 +1,205 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<style>
+
+    svg {
+        font: 10px sans-serif;
+    }
+
+    .line {
+        fill: none;
+        stroke: darkgreen;
+        stroke-width: 2px;
+    }
+
+    .axis path,
+    .axis line {
+        fill: none;
+        stroke: #999;
+        stroke-width: 2px;
+        shape-rendering: crispEdges;
+    }
+
+</style>
+<body>
+<script src="http://d3js.org/d3.v3.min.js"></script>
+<script>
+    (function () {
+        var cs = 0,
+                samples = [
+                    89.53,
+                    37515.81,
+                    104609.6,
+                    113105.11,
+                    103194.74,
+                    122151.63,
+                    128623.9,
+                    137325.33,
+                    154897.31,
+                    161235.07,
+                    162025.4,
+                    164902.64,
+                    158196.26,
+                    161072.44,
+                    160792.54,
+                    164692.44,
+                    161979.74,
+                    162137.4,
+                    159325.19,
+                    170465.44,
+                    168186.46,
+                    171152.34,
+                    168221.02,
+                    167440.73,
+                    165003.39,
+                    166855.18,
+                    157268.79,
+                    164087.54,
+                    162265.21,
+                    165990.16,
+                    176364.01,
+                    172064.07,
+                    184872.24,
+                    183249.8,
+                    182282.47,
+                    171475.11,
+                    158880.58,
+                    166016.69,
+                    168233.16,
+                    177759.92,
+                    179742.87,
+                    170819.44,
+                    167577.73,
+                    169479.9,
+                    175544.89,
+                    183792.01,
+                    184689.52,
+                    178503.87,
+                    173219.27,
+                    179085.49,
+                    179700.54,
+                    174281.17,
+                    181353.08,
+                    180173.14,
+                    184093.16,
+                    186011.5,
+                    176952.79,
+                    175319.2,
+                    169001.05,
+                    174545.12,
+                    169156.29,
+                    171804.3,
+                    159155.54,
+                    154709.96,
+                    157263.97
+                ],
+                theSample,
+                headers = [ "Whole", "Half", "Third" ];
+
+        var n = 243,
+                duration = 750,
+                now = new Date(Date.now() - duration),
+                data = [];
+
+        headers.forEach(function (d, li) {
+            data[li] = d3.range(n).map(function () { return 0; });
+        });
+
+        var margin = {top: 20, right: 100, bottom: 20, left: 100},
+                width = 960 - margin.right,
+                height = 512 - margin.top - margin.bottom;
+
+        var x = d3.time.scale()
+                .domain([now - (n - 2) * duration, now - duration])
+                .range([0, width]);
+
+        var y = d3.scale.linear()
+                .domain([0, 200000])
+                .range([height, 0]);
+
+        var svg = d3.select("body").append("p").append("svg")
+                .attr("width", width + margin.left + margin.right)
+                .attr("height", height + margin.top + margin.bottom)
+                .append("g")
+                .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
+
+        svg.append("defs").append("clipPath")
+                .attr("id", "clip")
+                .append("rect")
+                .attr("width", width)
+                .attr("height", height);
+
+        var axis = svg.append("g")
+                .attr("class", "x axis")
+                .attr("transform", "translate(0," + height + ")")
+                .call(x.axis = d3.svg.axis().scale(x).orient("bottom"));
+
+        svg.append("g")
+                .attr("class", "y axis")
+                .call(d3.svg.axis().scale(y).orient("left"));
+
+        svg.append("g")
+                .attr("class", "y axis")
+                .attr("transform", "translate(" + width + " ,0)")
+                .call(d3.svg.axis().scale(y).orient("right"));
+
+        var lines = [], paths = [];
+        data.forEach(function (p, li) {
+            lines[li]= d3.svg.line()
+                    .interpolate("basis")
+                    .x(function (d, i) {
+                        return x(now - (n - 1 - i) * duration);
+                    })
+                    .y(function (d, i) {
+                        return y(d);
+                    });
+
+            paths[li] = svg.append("g")
+                    .attr("clip-path", "url(#clip)")
+                    .append("path")
+                    .datum(function () { return data[li]; })
+                    .attr("id", "line" + li)
+                    .attr("class", "line");
+        });
+
+        var transition = d3.select({}).transition()
+                .duration(750)
+                .ease("linear");
+
+        function tick() {
+            transition = transition.each(function () {
+                // update the domains
+                now = new Date();
+                x.domain([now - (n - 2) * duration, now - duration]);
+
+                data.forEach(function (d, li) {
+                    // push the new most recent sample onto the back
+                    d.push(theSample[li]);
+
+                   // redraw the line and slide it left
+                    paths[li].attr("d", lines[li]).attr("transform", null);
+                    paths[li].transition().attr("transform", "translate(" + x(now - (n - 1) * duration) + ")");
+
+                    // pop the old data point off the front
+                    d.shift();
+                });
+
+                // slide the x-axis left
+                axis.call(x.axis);
+
+            }).transition().each("start", tick);
+        }
+
+        function setSample() {
+            var v = samples[cs++];
+            theSample = [ v, v/2, v/3 ];
+        }
+
+        setSample();
+        setInterval(setSample, 1000);
+        tick();
+
+    })()
+</script>
+</body>
+</html>
diff --git a/web/gui/src/main/webapp/app/common.css b/web/gui/src/main/webapp/app/common.css
index 34f880d..8963ff5 100644
--- a/web/gui/src/main/webapp/app/common.css
+++ b/web/gui/src/main/webapp/app/common.css
@@ -21,7 +21,7 @@
 /* ------ for summary-list tables ------ */
 
 table.summary-list {
-    margin: 0 50px 50px 50px;
+    margin: 0 20px 16px 12px;
     font-size: 10pt;
     border-spacing: 0;
 }
diff --git a/web/gui/src/main/webapp/app/onos.css b/web/gui/src/main/webapp/app/onos.css
index 530c06b..b42af35 100644
--- a/web/gui/src/main/webapp/app/onos.css
+++ b/web/gui/src/main/webapp/app/onos.css
@@ -48,6 +48,10 @@
     height: 100%;
 }
 
+#view h2 {
+    padding-left: 12px;
+}
+
 .light #view h2 {
     color: #800;
 }