ONOS-4359: continued work on theming UI
- topo view: device icon and label re-theming (WIP)

Change-Id: I5ecbc1c5b8a8315bfadfaacf62cfdb0e6d1f5a9c
(cherry picked from commit 92eaf44)
diff --git a/web/gui/src/main/webapp/app/fw/svg/icon.js b/web/gui/src/main/webapp/app/fw/svg/icon.js
index 6a541ce..5cc4171 100644
--- a/web/gui/src/main/webapp/app/fw/svg/icon.js
+++ b/web/gui/src/main/webapp/app/fw/svg/icon.js
@@ -149,8 +149,7 @@
     // configuration for device and host icons in the topology view
     var config = {
         device: {
-            dim: 36,
-            rx: 4
+            dim: 36
         },
         host: {
             badge: {
@@ -170,30 +169,15 @@
     };
 
 
-    // Adds a device icon to the specified element, using the given glyph.
-    // Returns the D3 selection of the icon.
-    function addDeviceIcon(elem, glyphId) {
-        var cfg = config.device,
-            gid = gs.glyphDefined(glyphId) ? glyphId : 'query',
-            g = elem.append('g')
-                .attr('class', 'svgIcon deviceIcon');
-
-        g.append('rect').attr({
-            x: 0,
-            y: 0,
-            rx: cfg.rx,
-            width: cfg.dim,
-            height: cfg.dim
-        });
-
-        g.append('use').attr({
+    // Adds a device glyph to the specified element.
+    // Returns the D3 selection of the glyph (use) element.
+    function addDeviceIcon(elem, glyphId, iconDim) {
+        var gid = gs.glyphDefined(glyphId) ? glyphId : 'query';
+        return elem.append('use').attr({
             'xlink:href': '#' + gid,
-            width: cfg.dim,
-            height: cfg.dim
+            width: iconDim,
+            height: iconDim
         });
-
-        g.dim = cfg.dim;
-        return g;
     }
 
     function addHostIcon(elem, radius, glyphId) {
diff --git a/web/gui/src/main/webapp/app/view/topo/topo-theme.css b/web/gui/src/main/webapp/app/view/topo/topo-theme.css
index 052ef61..197b6d6 100644
--- a/web/gui/src/main/webapp/app/view/topo/topo-theme.css
+++ b/web/gui/src/main/webapp/app/view/topo/topo-theme.css
@@ -20,23 +20,23 @@
 
 /* --- Base SVG Layer --- */
 
-.light #ov-topo svg {
+#ov-topo svg {
     background-color: #f4f4f4;
 }
 
 /* --- "No Devices" Layer --- */
 
-.light #ov-topo svg .noDevsBird {
+#ov-topo svg .noDevsBird {
     fill: #db7773;
 }
 
-.light #ov-topo svg #topo-noDevsLayer text {
+#ov-topo svg #topo-noDevsLayer text {
     fill: #7e9aa8;
 }
 
 /* --- Topo Map --- */
 
-.light #ov-topo svg #topo-map {
+#ov-topo svg #topo-map {
     stroke-width: 2px;
     stroke: #f4f4f4;
     fill: #e5e5e6;
@@ -44,19 +44,19 @@
 
 /* --- general topo-panel styling --- */
 
-.light .topo-p svg .glyph {
+.topo-p svg .glyph {
     fill: #222;
 }
 
-.light .topo-p svg .glyph.overlay {
+.topo-p svg .glyph.overlay {
     fill: #fff;
 }
 
-.light .topo-p h2 {
+.topo-p h2 {
     color: black;
 }
 
-.light .topo-p h3 {
+.topo-p h3 {
     color: black;
 }
 
@@ -67,7 +67,7 @@
 .topo-p td.value {
 }
 
-.light .topo-p hr {
+.topo-p hr {
     background-color: #ccc;
     color: #ccc;
 }
@@ -76,18 +76,14 @@
 
 #topo-p-instance svg rect {
     stroke-width: 0;
-}
-#topo-p-instance .online svg rect {
-    opacity: 1;
-}
-.light #topo-p-instance svg rect {
-    fill: #fbfbfb;
-}
-/* body of an instance */
-.light #topo-p-instance .online svg rect {
     fill: #fbfbfb;
 }
 
+/* body of an instance */
+#topo-p-instance .online svg rect {
+    opacity: 1;
+    fill: #fbfbfb;
+}
 
 #topo-p-instance svg .glyph {
     fill: #fff;
@@ -100,19 +96,15 @@
 /* offline */
 #topo-p-instance svg .badgeIcon {
     opacity: 0.4;
-}
-.light #topo-p-instance svg .badgeIcon {
     fill: #939598;
 }
 
 /* online */
 #topo-p-instance .online svg .badgeIcon {
     opacity: 1.0;
-}
-.light #topo-p-instance .online svg .badgeIcon {
     fill: #939598;
 }
-.light #topo-p-instance .online svg .badgeIcon.bird {
+#topo-p-instance .online svg .badgeIcon.bird {
     fill: #ffffff;
 }
 
@@ -126,14 +118,11 @@
 #topo-p-instance svg text {
     text-anchor: left;
     opacity: 0.5;
-}
-#topo-p-instance .online svg text {
-    opacity: 1.0;
-}
-.light #topo-p-instance svg text {
     fill: #3c3a3a;
 }
-.light #topo-p-instance .online svg text {
+
+#topo-p-instance .online svg text {
+    opacity: 1.0;
     fill: #3c3a3a;
 }
 
@@ -143,11 +132,11 @@
 #topo-p-instance .onosInst.mastership.affinity {
     opacity: 1.0;
 }
-.light #topo-p-instance .onosInst.mastership.affinity svg rect {
+#topo-p-instance .onosInst.mastership.affinity svg rect {
     filter: url(#blue-glow);
 }
 
-.light.firefox #topo-p-instance .onosInst.mastership.affinity svg rect {
+.firefox #topo-p-instance .onosInst.mastership.affinity svg rect {
     filter: url("data:image/svg+xml;utf8, <svg xmlns = \'http://www.w3.org/2000/svg\'><filter x=\"-50%\" y=\"-50%\" width=\"200%\" height=\"200%\" id=\"blue-glow\"><feColorMatrix type=\"matrix\" values=\"0 0 0 0  0 0 0 0 0  0 0 0 0 0  0.7 0 0 0 1  0 \"></feColorMatrix><feGaussianBlur stdDeviation=\"3\" result=\"coloredBlur\"></feGaussianBlur><feMerge><feMergeNode in=\"coloredBlur\"></feMergeNode><feMergeNode in=\"SourceGraphic\"></feMergeNode></feMerge></filter></svg>#blue-glow");
 }
 
@@ -161,84 +150,51 @@
     opacity: 0.2 !important;
 }
 
-.light #ov-topo svg .node.selected rect,
-.light #ov-topo svg .node.selected circle {
+#ov-topo svg .node.selected rect,
+#ov-topo svg .node.selected circle {
     fill: #f90;
     filter: url(#blue-glow);
 }
-/*.dark #ov-topo svg .node.selected rect,*/
-/*.dark #ov-topo svg .node.selected circle {*/
-    /*fill: #f90;*/
-    /*filter: url(#yellow-glow);*/
-/*}*/
-.light.firefox #ov-topo svg .node.selected rect,
-.light.firefox #ov-topo svg .node.selected circle {
+.firefox #ov-topo svg .node.selected rect,
+.firefox #ov-topo svg .node.selected circle {
     filter: url("data:image/svg+xml;utf8, <svg xmlns = \'http://www.w3.org/2000/svg\'><filter x=\"-50%\" y=\"-50%\" width=\"200%\" height=\"200%\" id=\"blue-glow\"><feColorMatrix type=\"matrix\" values=\"0 0 0 0  0 0 0 0 0  0 0 0 0 0  0.7 0 0 0 1  0 \"></feColorMatrix><feGaussianBlur stdDeviation=\"3\" result=\"coloredBlur\"></feGaussianBlur><feMerge><feMergeNode in=\"coloredBlur\"></feMergeNode><feMergeNode in=\"SourceGraphic\"></feMergeNode></feMerge></filter></svg>#blue-glow");
 }
-/*.dark.firefox #ov-topo svg .node.selected rect,*/
-/*.dark.firefox #ov-topo svg .node.selected circle {*/
-    /*filter: url("data:image/svg+xml;utf8, <svg xmlns = \'http://www.w3.org/2000/svg\'><filter x=\"-50%\" y=\"-50%\" width=\"200%\" height=\"200%\" id=\"yellow-glow\"><feColorMatrix type=\"matrix\" values=\"0 0 0 0  1.0 0 0 0 0  1.0 0 0 0 0  0.3 0 0 0 1  0 \"></feColorMatrix><feGaussianBlur stdDeviation=\"3\" result=\"coloredBlur\"></feGaussianBlur><feMerge><feMergeNode in=\"coloredBlur\"></feMergeNode><feMergeNode in=\"SourceGraphic\"></feMergeNode></feMerge></filter></svg>#yellow-glow");*/
-/*}*/
 
 /* Device Nodes */
 
+/* note: device without the 'online' class is offline */
 #ov-topo svg .node.device rect {
-    stroke-width: 1.5;
+    /* TODO: theme */
+    fill: #f0f0f0;
 }
-
-#ov-topo svg .node.device.fixed rect {
-    stroke-width: 1.5;
-}
-.light #ov-topo svg .node.device.fixed rect {
-    stroke: #aaa;
-}
-
-/* note: device is offline without the 'online' class */
-.light #ov-topo svg .node.device {
-    fill: #777;
-}
-
-.light #ov-topo svg .node.device rect {
-    stroke: #666;
-}
-.light #ov-topo svg .node.device rect {
-    stroke: #999;
-}
-
-.light #ov-topo svg .node.device.online {
-    fill: #6e7fa3;
-}
-
-/* note: device is offline without the 'online' class */
 #ov-topo svg .node.device text {
+    /*TODO: theme*/
     fill: #bbb;
 }
-
-#ov-topo svg .node.device.online text {
-    fill: white;
-}
-
-#ov-topo svg .node.device .svgIcon rect {
-    fill: #aaa;
-}
-#ov-topo svg .node.device .svgIcon use {
+#ov-topo svg .node.device use {
+    /*TODO: theme*/
     fill: #777;
 }
-#ov-topo svg .node.device.selected .svgIcon rect {
-    fill: #f90;
+
+
+#ov-topo svg .node.device.online rect {
+    fill: #ffffff;
 }
-#ov-topo svg .node.device.online .svgIcon rect {
-    fill: #ccc;
+#ov-topo svg .node.device.online text {
+    fill: #3c3a3a;
 }
-#ov-topo svg .node.device.online .svgIcon use {
-    fill: #000;
+#ov-topo svg .node.device.online use {
+    /* NOTE: this gets overridden programatically */
+    fill: #454545;
 }
-#ov-topo svg .node.device.online.selected .svgIcon rect {
+
+
+#ov-topo svg .node.device.selected rect {
     fill: #f90;
 }
 
 /* Badges */
-/* (... works for both light and dark themes...) */
+/* (... works for bothand dark themes...) */
 #ov-topo svg .node .badge circle {
     stroke: #aaa;
 }
@@ -279,17 +235,15 @@
 #ov-topo svg .node.host text {
     stroke: none;
     font: 9pt sans-serif;
-}
-.light #ov-topo svg .node.host text {
     fill: #846;
 }
 
-.light svg .node.host circle {
-    stroke: #000;
-    fill: #edb;
+svg .node.host circle {
+    stroke: #a3a596;
+    fill: #e0dfd6;
 }
 
-.light svg .node.host .svgIcon {
+svg .node.host .svgIcon {
     fill: #444;
 }
 
@@ -302,24 +256,12 @@
 #ov-topo svg .link.selected,
 #ov-topo svg .link.enhanced {
     stroke-width: 4.5px;
-}
-.light #ov-topo svg .link.selected,
-.light #ov-topo svg .link.enhanced {
     filter: url(#blue-glow);
 }
-/*.dark #ov-topo svg .link.selected,*/
-/*.dark #ov-topo svg .link.enhanced {*/
-    /*filter: url(#yellow-glow);*/
-/*}*/
-.light.firefox #ov-topo svg .link.selected,
-.light.firefox #ov-topo svg .link.enhanced {
+.firefox #ov-topo svg .link.selected,
+.firefox #ov-topo svg .link.enhanced {
     filter: url("data:image/svg+xml;utf8, <svg xmlns = \'http://www.w3.org/2000/svg\'><filter x=\"-50%\" y=\"-50%\" width=\"200%\" height=\"200%\" id=\"blue-glow\"><feColorMatrix type=\"matrix\" values=\"0 0 0 0  0 0 0 0 0  0 0 0 0 0  0.7 0 0 0 1  0 \"></feColorMatrix><feGaussianBlur stdDeviation=\"3\" result=\"coloredBlur\"></feGaussianBlur><feMerge><feMergeNode in=\"coloredBlur\"></feMergeNode><feMergeNode in=\"SourceGraphic\"></feMergeNode></feMerge></filter></svg>#blue-glow");
 }
-/*.dark.firefox #ov-topo svg .link.selected,*/
-/*.dark.firefox #ov-topo svg .link.enhanced {*/
-    /*filter: url("data:image/svg+xml;utf8, <svg xmlns = \'http://www.w3.org/2000/svg\'><filter x=\"-50%\" y=\"-50%\" width=\"200%\" height=\"200%\" id=\"yellow-glow\"><feColorMatrix type=\"matrix\" values=\"0 0 0 0  1.0 0 0 0 0  1.0 0 0 0 0  0.3 0 0 0 1  0 \"></feColorMatrix><feGaussianBlur stdDeviation=\"3\" result=\"coloredBlur\"></feGaussianBlur><feMerge><feMergeNode in=\"coloredBlur\"></feMergeNode><feMergeNode in=\"SourceGraphic\"></feMergeNode></feMerge></filter></svg>#yellow-glow");*/
-
-/*}*/
 
 #ov-topo svg .link.inactive {
     opacity: .5;
@@ -334,29 +276,27 @@
 
 #ov-topo svg .link.secondary {
     stroke-width: 3px;
-}
-.light #ov-topo svg .link.secondary {
     stroke: rgba(0,153,51,0.5);
 }
 
 /* Port traffic color visualization for Kbps, Mbps, and Gbps */
 
-.light #ov-topo svg .link.secondary.port-traffic-Kbps {
+#ov-topo svg .link.secondary.port-traffic-Kbps {
     stroke: rgb(0,153,51);
     stroke-width: 5.0;
 }
 
-.light #ov-topo svg .link.secondary.port-traffic-Mbps {
+#ov-topo svg .link.secondary.port-traffic-Mbps {
     stroke: rgb(128,145,27);
     stroke-width: 6.5;
 }
 
-.light #ov-topo svg .link.secondary.port-traffic-Gbps {
+#ov-topo svg .link.secondary.port-traffic-Gbps {
     stroke: rgb(255, 137, 3);
     stroke-width: 8.0;
 }
 
-.light #ov-topo svg .link.secondary.port-traffic-Gbps-choked {
+#ov-topo svg .link.secondary.port-traffic-Gbps-choked {
     stroke: rgb(183, 30, 21);
     stroke-width: 8.0;
 }
@@ -380,34 +320,26 @@
 
 #ov-topo svg .link.primary {
     stroke-width: 4px;
-}
-.light #ov-topo svg .link.primary {
     stroke: #ffA300;
 }
 
 #ov-topo svg .link.secondary.optical {
     stroke-width: 4px;
-}
-.light #ov-topo svg .link.secondary.optical {
     stroke: rgba(128,64,255,0.5);
 }
 
 #ov-topo svg .link.primary.optical {
     stroke-width: 6px;
-}
-.light #ov-topo svg .link.primary.optical {
     stroke: #74f;
 }
 
 /* Link Labels */
 #ov-topo svg .linkLabel rect {
     stroke: none;
-}
-.light #ov-topo svg .linkLabel rect {
     fill: #eee;
 }
 
-.light #ov-topo svg .linkLabel text {
+#ov-topo svg .linkLabel text {
     fill: #444;
 }
 
@@ -415,54 +347,52 @@
 
 #ov-topo svg .portLabel rect {
     stroke: none;
-}
-.light #ov-topo svg .portLabel rect {
     fill: #eee;
 }
 
-.light #ov-topo svg .portLabel text {
+#ov-topo svg .portLabel text {
     fill: #444;
 }
 
 /* Number of Links Labels */
 
 
-.light #ov-topo text.numLinkText {
+#ov-topo text.numLinkText {
     fill: #444;
 }
 
 /* ------------------------------------------------- */
 /* Sprite Layer */
 
-.light #ov-topo svg #topo-sprites .gold1 use {
+#ov-topo svg #topo-sprites .gold1 use {
     stroke: #fda;
     fill: none;
 }
-.light #ov-topo svg #topo-sprites .gold1 text {
+#ov-topo svg #topo-sprites .gold1 text {
     fill: #eda;
 }
 
-.light #ov-topo svg #topo-sprites .blue1 use {
+#ov-topo svg #topo-sprites .blue1 use {
     stroke: #bbd;
     fill: none;
 }
-.light #ov-topo svg #topo-sprites .blue1 text {
+#ov-topo svg #topo-sprites .blue1 text {
     fill: #cce;
 }
 
-.light #ov-topo svg #topo-sprites .gray1 use {
+#ov-topo svg #topo-sprites .gray1 use {
     stroke: #ccc;
     fill: none;
 }
-.light #ov-topo svg #topo-sprites .gray1 text {
+#ov-topo svg #topo-sprites .gray1 text {
     fill: #ddd;
 }
 
 /* fills */
-.light #ov-topo svg #topo-sprites use.fill-gray2 {
+#ov-topo svg #topo-sprites use.fill-gray2 {
     fill: #eee;
 }
 
-.light #ov-topo svg #topo-sprites use.fill-blue2 {
+#ov-topo svg #topo-sprites use.fill-blue2 {
     fill: #bce;
 }
diff --git a/web/gui/src/main/webapp/app/view/topo/topo.css b/web/gui/src/main/webapp/app/view/topo/topo.css
index 7b2fb98..0f016b6 100644
--- a/web/gui/src/main/webapp/app/view/topo/topo.css
+++ b/web/gui/src/main/webapp/app/view/topo/topo.css
@@ -179,6 +179,7 @@
 
 #ov-topo svg .node {
     cursor: pointer;
+    fill-rule: evenodd;
 }
 
 #ov-topo svg .node text {
diff --git a/web/gui/src/main/webapp/app/view/topo/topoD3.js b/web/gui/src/main/webapp/app/view/topo/topoD3.js
index d6cf986..278fc25 100644
--- a/web/gui/src/main/webapp/app/view/topo/topoD3.js
+++ b/web/gui/src/main/webapp/app/view/topo/topoD3.js
@@ -39,28 +39,17 @@
      */
 
     // configuration
-    var devCfg = {
-            xoff: -20,
-            yoff: -18
-        },
-        labelConfig = {
-            imgPad: 16,
-            padLR: 4,
-            padTB: 3,
-            marginLR: 3,
-            marginTB: 2,
-            port: {
-                gap: 3,
-                width: 18,
-                height: 14
-            }
-        },
-        badgeConfig = {
+    var devIconDim = 36;
+    var labelPad = 4;
+
+    var badgeConfig = {
             radius: 12,
             yoff: 5,
             gdelta: 10
-        },
-        icfg;
+        };
+
+    // TODO: remove dependence on this
+    var icfg;
 
     var status = {
         i: 'badgeInfo',
@@ -87,77 +76,32 @@
     var deviceLabelIndex = 0,
         hostLabelIndex = 0;
 
-
-    var dCol = {
-        black: '#000',
-        paleblue: '#acf',
-        offwhite: '#ddd',
-        darkgrey: '#444',
-        midgrey: '#888',
-        lightgrey: '#bbb',
-        orange: '#f90'
-    };
-
     // note: these are the device icon colors without affinity
     var dColTheme = {
         light: {
-            rfill: dCol.offwhite,
-            online: {
-                glyph: dCol.darkgrey,
-                rect: dCol.paleblue
-            },
-            offline: {
-                glyph: dCol.midgrey,
-                rect: dCol.lightgrey
-            }
+            online: '#444444',
+            offline: '#cccccc'
         },
         dark: {
-            rfill: dCol.midgrey,
-            online: {
-                glyph: dCol.darkgrey,
-                rect: dCol.paleblue
-            },
-            offline: {
-                glyph: dCol.midgrey,
-                rect: dCol.darkgrey
-            }
+            // TODO: theme
+            online: '#444444',
+            offline: '#cccccc'
         }
     };
 
-    function devBaseColor(d) {
-        var o = d.online ? 'online' : 'offline';
-        return dColTheme[ts.theme()][o];
+    function devGlyphColor(d) {
+        var o = d.online,
+            id = d.master,
+            otag = o ? 'online' : 'offline';
+        return o ? sus.cat7().getColor(id, 0, ts.theme())
+                 : dColTheme[ts.theme()][otag];
     }
 
     function setDeviceColor(d) {
-        var o = d.online,
-            s = d.el.classed('selected'),
-            c = devBaseColor(d),
-            a = instColor(d.master, o),
-            icon = d.el.select('g.deviceIcon'),
-            g, r;
-
-        if (s) {
-            g = c.glyph;
-            r = dCol.orange;
-        } else if (api.instVisible()) {
-            g = o ? a : c.glyph;
-            r = o ? c.rfill : a;
-        } else {
-            g = c.glyph;
-            r = c.rect;
-        }
-
-        icon.select('use').style('fill', g);
-        icon.select('rect').style('fill', r);
+        d.el.select('use')
+            .style('fill', devGlyphColor(d));
     }
 
-    function instColor(id, online) {
-        return sus.cat7().getColor(id, !online, ts.theme());
-    }
-
-    // ====
-
     function incDevLabIndex() {
         setDevLabIndex(deviceLabelIndex+1);
         switch(deviceLabelIndex) {
@@ -174,82 +118,51 @@
         ps.setPrefs('topo_prefs', p);
     }
 
-    // Returns the newly computed bounding box of the rectangle
-    function adjustRectToFitText(n) {
-        var text = n.select('text'),
-            box = text.node().getBBox(),
-            lab = labelConfig;
-
-        text.attr('text-anchor', 'middle')
-            .attr('y', '-0.8em')
-            .attr('x', lab.imgPad/2);
-
-        // translate the bbox so that it is centered on [x,y]
-        box.x = -box.width / 2;
-        box.y = -box.height / 2;
-
-        // add padding
-        box.x -= (lab.padLR + lab.imgPad/2);
-        box.width += lab.padLR * 2 + lab.imgPad;
-        box.y -= lab.padTB;
-        box.height += lab.padTB * 2;
-
-        return box;
-    }
-
     function hostLabel(d) {
         var idx = (hostLabelIndex < d.labels.length) ? hostLabelIndex : 0;
         return d.labels[idx];
     }
+
     function deviceLabel(d) {
         var idx = (deviceLabelIndex < d.labels.length) ? deviceLabelIndex : 0;
         return d.labels[idx];
     }
+
     function trimLabel(label) {
         return (label && label.trim()) || '';
     }
 
-    function emptyBox() {
+    function computeLabelWidth(n) {
+        var text = n.select('text'),
+            box = text.node().getBBox();
+        return box.width + labelPad * 2;
+    }
+
+    function iconBox(dim, labelWidth) {
         return {
-            x: -2,
-            y: -2,
-            width: 4,
-            height: 4
-        };
+            x: -dim/2,
+            y: -dim/2,
+            width: dim + labelWidth,
+            height: dim
+        }
     }
 
     function updateDeviceRendering(d) {
-        var label = trimLabel(deviceLabel(d)),
-            noLabel = !label,
-            node = d.el,
-            dim = icfg.device.dim,
-            box, dx, dy,
-            bdg = d.badge;
+        var node = d.el,
+            bdg = d.badge,
+            label = trimLabel(deviceLabel(d)),
+            labelWidth;
 
-        node.select('text')
-            .text(label);
-
-        if (noLabel) {
-            box = emptyBox();
-            dx = -dim/2;
-            dy = -dim/2;
-        } else {
-            box = adjustRectToFitText(node);
-            dx = box.x + devCfg.xoff;
-            dy = box.y + devCfg.yoff;
-        }
+        node.select('text').text(label);
+        labelWidth = label ? computeLabelWidth(node) : 0;
 
         node.select('rect')
             .transition()
-            .attr(box);
+            .attr(iconBox(devIconDim, labelWidth));
 
-        node.select('g.deviceIcon')
-            .transition()
-            .attr('transform', sus.translate(dx, dy));
-
-        // handle badge, if defined
+        // TODO: verify badge placement
         if (bdg) {
-            renderBadge(node, bdg, { dx: dx + dim, dy: dy });
+            renderBadge(node, bdg, { dx: devIconDim, dy: 0 });
         }
     }
 
@@ -259,7 +172,6 @@
 
         updateHostLabel(d);
 
-        // handle badge, if defined
         if (bdg) {
             renderBadge(node, bdg, icfg.host.badge);
         }
@@ -331,28 +243,26 @@
         var node = d3.select(this),
             glyphId = mapDeviceTypeToGlyph(d.type),
             label = trimLabel(deviceLabel(d)),
-            noLabel = !label,
-            box, dx, dy, icon;
+            xlate = -devIconDim/2,
+            rect, text, glyph, labelWidth;
 
         d.el = node;
 
-        node.append('rect').attr({ rx: 5, ry: 5 });
-        node.append('text').text(label).attr('dy', '1.1em');
-        box = adjustRectToFitText(node);
-        node.select('rect').attr(box);
+        rect = node.append('rect');
 
-        icon = is.addDeviceIcon(node, glyphId);
+        text = node.append('text').text(label)
+            .attr('text-anchor', 'left')
+            .attr('y', '0.3em')
+            .attr('x', devIconDim / 2 + labelPad);
 
-        if (noLabel) {
-            dx = -icon.dim/2;
-            dy = -icon.dim/2;
-        } else {
-            box = adjustRectToFitText(node);
-            dx = box.x + devCfg.xoff;
-            dy = box.y + devCfg.yoff;
-        }
+        glyph = is.addDeviceIcon(node, glyphId, devIconDim);
 
-        icon.attr('transform', sus.translate(dx, dy));
+        labelWidth = label ? computeLabelWidth(node) : 0;
+
+        rect.attr(iconBox(devIconDim, labelWidth));
+        glyph.attr(iconBox(devIconDim, 0));
+
+        node.attr('transform', sus.translate(xlate, xlate));
     }
 
     function hostEnter(d) {
@@ -631,7 +541,6 @@
 
                 incDevLabIndex: incDevLabIndex,
                 setDevLabIndex: setDevLabIndex,
-                adjustRectToFitText: adjustRectToFitText,
                 hostLabel: hostLabel,
                 deviceLabel: deviceLabel,
                 trimLabel: trimLabel,
diff --git a/web/gui/src/main/webapp/app/view/topo/topoForce.js b/web/gui/src/main/webapp/app/view/topo/topoForce.js
index c46d3a4..6af88a3 100644
--- a/web/gui/src/main/webapp/app/view/topo/topoForce.js
+++ b/web/gui/src/main/webapp/app/view/topo/topoForce.js
@@ -29,14 +29,15 @@
     // configuration
     var linkConfig = {
         light: {
-            baseColor: '#666',
+            baseColor: '#939598',
             inColor: '#66f',
             outColor: '#f00'
         },
         dark: {
-            baseColor: '#aaa',
+            // TODO : theme
+            baseColor: '#939598',
             inColor: '#66f',
-            outColor: '#f66'
+            outColor: '#f00'
         },
         inWidth: 12,
         outWidth: 10
@@ -337,7 +338,7 @@
             modeCls = ldata.expected() ? 'inactive' : 'not-permitted',
             delay = immediate ? 0 : 1000;
 
-        // FIXME: understand why el is sometimes undefined on addLink events...
+        // NOTE: understand why el is sometimes undefined on addLink events...
         // Investigated:
         // el is undefined when it's a reverse link that is being added.
         // updateLinks (which sets ldata.el) isn't called before this is called.
diff --git a/web/gui/src/main/webapp/data/img/masthead-logo-mojo.png b/web/gui/src/main/webapp/data/img/masthead-logo-mojo.png
index 46cc874..969c5fb 100644
--- a/web/gui/src/main/webapp/data/img/masthead-logo-mojo.png
+++ b/web/gui/src/main/webapp/data/img/masthead-logo-mojo.png
Binary files differ
diff --git a/web/gui/src/main/webapp/data/img/nav-menu-mojo.png b/web/gui/src/main/webapp/data/img/nav-menu-mojo.png
index aa3fd1b..4dd3541 100644
--- a/web/gui/src/main/webapp/data/img/nav-menu-mojo.png
+++ b/web/gui/src/main/webapp/data/img/nav-menu-mojo.png
Binary files differ
diff --git a/web/gui/src/test/_karma/ev/mojo/ev_17_updInst_1_ready.json b/web/gui/src/test/_karma/ev/mojo/ev_17_updInst_1_ready.json
new file mode 100644
index 0000000..24491dd
--- /dev/null
+++ b/web/gui/src/test/_karma/ev/mojo/ev_17_updInst_1_ready.json
@@ -0,0 +1,11 @@
+{
+  "event": "updateInstance",
+  "payload": {
+    "id": "192.168.56.101",
+    "ip": "192.168.56.101",
+    "online": true,
+    "ready": true,
+    "uiAttached": true,
+    "switches": 0
+  }
+}
diff --git a/web/gui/src/test/_karma/ev/mojo/ev_18_updInst_2_ready.json b/web/gui/src/test/_karma/ev/mojo/ev_18_updInst_2_ready.json
new file mode 100644
index 0000000..4797767
--- /dev/null
+++ b/web/gui/src/test/_karma/ev/mojo/ev_18_updInst_2_ready.json
@@ -0,0 +1,11 @@
+{
+  "event": "updateInstance",
+  "payload": {
+    "id": "ONOS-2",
+    "ip": "192.168.56.102",
+    "online": true,
+    "ready": true,
+    "uiAttached": false,
+    "switches": 0
+  }
+}
diff --git a/web/gui/src/test/_karma/ev/mojo/ev_19_updInst_3_ready.json b/web/gui/src/test/_karma/ev/mojo/ev_19_updInst_3_ready.json
new file mode 100644
index 0000000..4194667
--- /dev/null
+++ b/web/gui/src/test/_karma/ev/mojo/ev_19_updInst_3_ready.json
@@ -0,0 +1,11 @@
+{
+  "event": "updateInstance",
+  "payload": {
+    "id": "ONOS-3",
+    "ip": "192.168.56.103",
+    "online": true,
+    "ready": true,
+    "uiAttached": false,
+    "switches": 0
+  }
+}
diff --git a/web/gui/src/test/_karma/ev/mojo/ev_20_addDevice_A_offline.json b/web/gui/src/test/_karma/ev/mojo/ev_20_addDevice_A_offline.json
new file mode 100644
index 0000000..81c207c
--- /dev/null
+++ b/web/gui/src/test/_karma/ev/mojo/ev_20_addDevice_A_offline.json
@@ -0,0 +1,23 @@
+{
+  "event": "addDevice",
+  "payload": {
+    "id": "of:0000ffffffff000a",
+    "type": "switch",
+    "online": false,
+    "master": "192.168.56.101",
+    "x-location": {
+      "type": "lnglat",
+      "lat": 37.7833,
+      "lng": -122.4167
+    },
+    "labels": [
+      "",
+      "sw-A",
+      "0000ffffffff000a"
+    ],
+    "metaUi": {
+      "x": 520,
+      "y": 350
+    }
+  }
+}
diff --git a/web/gui/src/test/_karma/ev/mojo/ev_21_addDevice_B_offline.json b/web/gui/src/test/_karma/ev/mojo/ev_21_addDevice_B_offline.json
new file mode 100644
index 0000000..521f75c
--- /dev/null
+++ b/web/gui/src/test/_karma/ev/mojo/ev_21_addDevice_B_offline.json
@@ -0,0 +1,23 @@
+{
+  "event": "addDevice",
+  "payload": {
+    "id": "of:0000ffffffff000b",
+    "type": "switch",
+    "online": false,
+    "master": "ONOS-2",
+    "x-location": {
+      "type": "lnglat",
+      "lat": 37.7833,
+      "lng": -120.4167
+    },
+    "labels": [
+      "",
+      "sw-B",
+      "0000ffffffff000b"
+    ],
+    "metaUi": {
+      "x": 720,
+      "y": 300
+    }
+  }
+}
diff --git a/web/gui/src/test/_karma/ev/mojo/ev_22_addDevice_C_offline.json b/web/gui/src/test/_karma/ev/mojo/ev_22_addDevice_C_offline.json
new file mode 100644
index 0000000..b5531cc
--- /dev/null
+++ b/web/gui/src/test/_karma/ev/mojo/ev_22_addDevice_C_offline.json
@@ -0,0 +1,23 @@
+{
+  "event": "addDevice",
+  "payload": {
+    "id": "of:0000ffffffff000c",
+    "type": "switch",
+    "online": false,
+    "master": "ONOS-3",
+    "x-location": {
+      "type": "lnglat",
+      "lat": 37.7833,
+      "lng": -118.4167
+    },
+    "labels": [
+      "",
+      "sw-C",
+      "0000ffffffff000c"
+    ],
+    "metaUi": {
+      "x": 920,
+      "y": 360
+    }
+  }
+}
diff --git a/web/gui/src/test/_karma/ev/mojo/ev_23_updDevice_A_online.json b/web/gui/src/test/_karma/ev/mojo/ev_23_updDevice_A_online.json
new file mode 100644
index 0000000..a843cda
--- /dev/null
+++ b/web/gui/src/test/_karma/ev/mojo/ev_23_updDevice_A_online.json
@@ -0,0 +1,23 @@
+{
+  "event": "addDevice",
+  "payload": {
+    "id": "of:0000ffffffff000a",
+    "type": "switch",
+    "online": true,
+    "master": "192.168.56.101",
+    "x-location": {
+      "type": "lnglat",
+      "lat": 37.7833,
+      "lng": -122.4167
+    },
+    "labels": [
+      "",
+      "sw-A",
+      "0000ffffffff000a"
+    ],
+    "metaUi": {
+      "x": 520,
+      "y": 350
+    }
+  }
+}
diff --git a/web/gui/src/test/_karma/ev/mojo/ev_24_updDevice_B_online.json b/web/gui/src/test/_karma/ev/mojo/ev_24_updDevice_B_online.json
new file mode 100644
index 0000000..96f0431
--- /dev/null
+++ b/web/gui/src/test/_karma/ev/mojo/ev_24_updDevice_B_online.json
@@ -0,0 +1,23 @@
+{
+  "event": "addDevice",
+  "payload": {
+    "id": "of:0000ffffffff000b",
+    "type": "switch",
+    "online": true,
+    "master": "ONOS-2",
+    "x-location": {
+      "type": "lnglat",
+      "lat": 37.7833,
+      "lng": -120.4167
+    },
+    "labels": [
+      "",
+      "sw-B",
+      "0000ffffffff000b"
+    ],
+    "metaUi": {
+      "x": 720,
+      "y": 300
+    }
+  }
+}
diff --git a/web/gui/src/test/_karma/ev/mojo/ev_25_updDevice_C_online.json b/web/gui/src/test/_karma/ev/mojo/ev_25_updDevice_C_online.json
new file mode 100644
index 0000000..3f371dc
--- /dev/null
+++ b/web/gui/src/test/_karma/ev/mojo/ev_25_updDevice_C_online.json
@@ -0,0 +1,23 @@
+{
+  "event": "addDevice",
+  "payload": {
+    "id": "of:0000ffffffff000c",
+    "type": "switch",
+    "online": true,
+    "master": "ONOS-3",
+    "x-location": {
+      "type": "lnglat",
+      "lat": 37.7833,
+      "lng": -118.4167
+    },
+    "labels": [
+      "",
+      "sw-C",
+      "0000ffffffff000c"
+    ],
+    "metaUi": {
+      "x": 920,
+      "y": 360
+    }
+  }
+}
diff --git a/web/gui/src/test/_karma/ev/mojo/ev_26_addLink_A_B.json b/web/gui/src/test/_karma/ev/mojo/ev_26_addLink_A_B.json
new file mode 100644
index 0000000..635e04e
--- /dev/null
+++ b/web/gui/src/test/_karma/ev/mojo/ev_26_addLink_A_B.json
@@ -0,0 +1,13 @@
+{
+  "event": "addLink",
+  "payload": {
+    "id": "of:0000ffffffff000a/11-of:0000ffffffff000b/10",
+    "type": "direct",
+    "online": true,
+    "linkWidth": 2,
+    "src": "of:0000ffffffff000a",
+    "srcPort": "11",
+    "dst": "of:0000ffffffff000b",
+    "dstPort": "10"
+  }
+}
diff --git a/web/gui/src/test/_karma/ev/mojo/ev_27_addLink_B_A.json b/web/gui/src/test/_karma/ev/mojo/ev_27_addLink_B_A.json
new file mode 100644
index 0000000..61c19e4
--- /dev/null
+++ b/web/gui/src/test/_karma/ev/mojo/ev_27_addLink_B_A.json
@@ -0,0 +1,13 @@
+{
+  "event": "addLink",
+  "payload": {
+    "id": "of:0000ffffffff000b/10-of:0000ffffffff000a/11",
+    "type": "direct",
+    "online": true,
+    "linkWidth": 2,
+    "src": "of:0000ffffffff000b",
+    "srcPort": "10",
+    "dst": "of:0000ffffffff000a",
+    "dstPort": "11"
+  }
+}
diff --git a/web/gui/src/test/_karma/ev/mojo/ev_28_addLink_A_C.json b/web/gui/src/test/_karma/ev/mojo/ev_28_addLink_A_C.json
new file mode 100644
index 0000000..9b8ddf2
--- /dev/null
+++ b/web/gui/src/test/_karma/ev/mojo/ev_28_addLink_A_C.json
@@ -0,0 +1,13 @@
+{
+  "event": "addLink",
+  "payload": {
+    "id": "of:0000ffffffff000a/12-of:0000ffffffff000c/10",
+    "type": "direct",
+    "online": true,
+    "linkWidth": 2,
+    "src": "of:0000ffffffff000a",
+    "srcPort": "12",
+    "dst": "of:0000ffffffff000c",
+    "dstPort": "10"
+  }
+}
diff --git a/web/gui/src/test/_karma/ev/mojo/ev_29_addLink_C_A.json b/web/gui/src/test/_karma/ev/mojo/ev_29_addLink_C_A.json
new file mode 100644
index 0000000..218dbc2
--- /dev/null
+++ b/web/gui/src/test/_karma/ev/mojo/ev_29_addLink_C_A.json
@@ -0,0 +1,13 @@
+{
+  "event": "addLink",
+  "payload": {
+    "id": "of:0000ffffffff000c/10-of:0000ffffffff000a/12",
+    "type": "direct",
+    "online": true,
+    "linkWidth": 2,
+    "src": "of:0000ffffffff000c",
+    "srcPort": "10",
+    "dst": "of:0000ffffffff000a",
+    "dstPort": "12"
+  }
+}
diff --git a/web/gui/src/test/_karma/ev/mojo/ev_30_addLink_B_C.json b/web/gui/src/test/_karma/ev/mojo/ev_30_addLink_B_C.json
new file mode 100644
index 0000000..7616e14
--- /dev/null
+++ b/web/gui/src/test/_karma/ev/mojo/ev_30_addLink_B_C.json
@@ -0,0 +1,13 @@
+{
+  "event": "addLink",
+  "payload": {
+    "id": "of:0000ffffffff000b/12-of:0000ffffffff000c/11",
+    "type": "direct",
+    "online": true,
+    "linkWidth": 2,
+    "src": "of:0000ffffffff000b",
+    "srcPort": "12",
+    "dst": "of:0000ffffffff000c",
+    "dstPort": "11"
+  }
+}
diff --git a/web/gui/src/test/_karma/ev/mojo/ev_31_addLink_C_B.json b/web/gui/src/test/_karma/ev/mojo/ev_31_addLink_C_B.json
new file mode 100644
index 0000000..c36aa3e
--- /dev/null
+++ b/web/gui/src/test/_karma/ev/mojo/ev_31_addLink_C_B.json
@@ -0,0 +1,13 @@
+{
+  "event": "addLink",
+  "payload": {
+    "id": "of:0000ffffffff000c/11-of:0000ffffffff000b/12",
+    "type": "direct",
+    "online": true,
+    "linkWidth": 2,
+    "src": "of:0000ffffffff000c",
+    "srcPort": "11",
+    "dst": "of:0000ffffffff000b",
+    "dstPort": "12"
+  }
+}
diff --git a/web/gui/src/test/_karma/ev/mojo/ev_32_addHost_A.json b/web/gui/src/test/_karma/ev/mojo/ev_32_addHost_A.json
new file mode 100644
index 0000000..69aa66e
--- /dev/null
+++ b/web/gui/src/test/_karma/ev/mojo/ev_32_addHost_A.json
@@ -0,0 +1,20 @@
+{
+  "event": "addHost",
+  "payload": {
+    "id": "0E:2A:69:30:13:86/-1",
+    "ingress": "0E:2A:69:30:13:86/-1/0-of:0000ffffffff000a/2",
+    "egress": "of:0000ffffffff000a/2-0E:2A:69:30:13:86/-1/0",
+    "cp": {
+      "device": "of:0000ffffffff000a",
+      "port": 2
+    },
+    "labels": [
+      "192.168.222.10",
+      "0E:2A:69:30:13:86"
+    ],
+    "metaUi": {
+      "Xx": 800,
+      "Xy": 180
+    }
+  }
+}
diff --git a/web/gui/src/test/_karma/ev/mojo/ev_33_addHost_B.json b/web/gui/src/test/_karma/ev/mojo/ev_33_addHost_B.json
new file mode 100644
index 0000000..c9a3ce3
--- /dev/null
+++ b/web/gui/src/test/_karma/ev/mojo/ev_33_addHost_B.json
@@ -0,0 +1,20 @@
+{
+  "event": "addHost",
+  "payload": {
+    "id": "0E:2A:69:30:13:87/-1",
+    "ingress": "0E:2A:69:30:13:87/-1/0-of:0000ffffffff000b/2",
+    "egress": "of:0000ffffffff000b/2-0E:2A:69:30:13:87/-1/0",
+    "cp": {
+      "device": "of:0000ffffffff000b",
+      "port": 2
+    },
+    "labels": [
+      "192.168.222.11",
+      "0E:2A:69:30:13:87"
+    ],
+    "metaUi": {
+      "Xx": 800,
+      "Xy": 180
+    }
+  }
+}
diff --git a/web/gui/src/test/_karma/ev/mojo/ev_34_addHost_C.json b/web/gui/src/test/_karma/ev/mojo/ev_34_addHost_C.json
new file mode 100644
index 0000000..6205ed9
--- /dev/null
+++ b/web/gui/src/test/_karma/ev/mojo/ev_34_addHost_C.json
@@ -0,0 +1,20 @@
+{
+  "event": "addHost",
+  "payload": {
+    "id": "0E:2A:69:30:13:88/-1",
+    "ingress": "0E:2A:69:30:13:88/-1/0-of:0000ffffffff000c/2",
+    "egress": "of:0000ffffffff000c/2-0E:2A:69:30:13:88/-1/0",
+    "cp": {
+      "device": "of:0000ffffffff000c",
+      "port": 2
+    },
+    "labels": [
+      "192.168.222.12",
+      "0E:2A:69:30:13:88"
+    ],
+    "metaUi": {
+      "Xx": 800,
+      "Xy": 180
+    }
+  }
+}
diff --git a/web/gui/src/test/_karma/ev/mojo/scenario.json b/web/gui/src/test/_karma/ev/mojo/scenario.json
index a9a1e50..62193b7 100644
--- a/web/gui/src/test/_karma/ev/mojo/scenario.json
+++ b/web/gui/src/test/_karma/ev/mojo/scenario.json
@@ -4,7 +4,7 @@
   ],
   "title": "Color-Tweaking Scenario for Mojo Palette",
   "params": {
-    "lastAuto": 7
+    "lastAuto": 19
   },
   "description": [
     "Press 'a' to load initial events.",