You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@qpid.apache.org by ea...@apache.org on 2018/05/29 09:36:54 UTC

qpid-dispatch git commit: DISPATCH-1014 Add link utilization visualization to console's topology page

Repository: qpid-dispatch
Updated Branches:
  refs/heads/master 58967e47f -> e54c632bc


DISPATCH-1014 Add link utilization visualization to console's topology page


Project: http://git-wip-us.apache.org/repos/asf/qpid-dispatch/repo
Commit: http://git-wip-us.apache.org/repos/asf/qpid-dispatch/commit/e54c632b
Tree: http://git-wip-us.apache.org/repos/asf/qpid-dispatch/tree/e54c632b
Diff: http://git-wip-us.apache.org/repos/asf/qpid-dispatch/diff/e54c632b

Branch: refs/heads/master
Commit: e54c632bca5f4cf0f54728f2f3bf1851e664caf1
Parents: 58967e4
Author: Ernest Allen <ea...@redhat.com>
Authored: Tue May 29 05:36:30 2018 -0400
Committer: Ernest Allen <ea...@redhat.com>
Committed: Tue May 29 05:36:30 2018 -0400

----------------------------------------------------------------------
 console/stand-alone/plugin/css/dispatch.css     |  19 +-
 console/stand-alone/plugin/css/dispatchpf.css   |   5 -
 .../stand-alone/plugin/html/qdrTopology.html    |  69 ++-
 .../plugin/js/topology/qdrTopology.js           | 139 ++---
 .../stand-alone/plugin/js/topology/traffic.js   | 531 ++++++++++++++-----
 5 files changed, 541 insertions(+), 222 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/e54c632b/console/stand-alone/plugin/css/dispatch.css
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/css/dispatch.css b/console/stand-alone/plugin/css/dispatch.css
index a815ca1..3f0c9db 100644
--- a/console/stand-alone/plugin/css/dispatch.css
+++ b/console/stand-alone/plugin/css/dispatch.css
@@ -34,27 +34,32 @@ svg:not(.active):not(.ctrl) {
 	stroke: #33F;
 	fill: #33F;
 }
-path.link.selected {
+path.link.selected:not(.traffic) {
   /* stroke-dasharray: 10,2; */
   stroke: #33F  !important;
 }
 
 path.link {
   fill: #000;
-  stroke: #000;
+  /* stroke: #000; */
   stroke-width: 4px;
   cursor: default;
 }
 
+path:not(.traffic) {
+    stroke: #000;
+}
 svg:not(.active):not(.ctrl) path.link {
   cursor: pointer;
 }
 
 path.link.small {
   stroke-width: 2.5;
-  stroke: darkgray;
 }
-path.link.highlighted {
+path.link.small:not(.traffic) {
+    stroke: darkgray;
+}
+path.link.highlighted:not(.traffic) {
     stroke: #6F6 !important;
 }
 marker#start-arrow-highlighted,
@@ -65,7 +70,9 @@ marker#start-arrow-small,
 marker#end-arrow-small {
     fill: darkgray;
 }
-
+marker {
+    stroke-width: 0;
+}
 path.link.dragline {
   pointer-events: none;
 }
@@ -700,4 +707,4 @@ select.required, input.required {
 .error {
   color: red;
   font-weight: bold;
-}
\ No newline at end of file
+}

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/e54c632b/console/stand-alone/plugin/css/dispatchpf.css
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/css/dispatchpf.css b/console/stand-alone/plugin/css/dispatchpf.css
index 49e7d4b..09b8eb7 100644
--- a/console/stand-alone/plugin/css/dispatchpf.css
+++ b/console/stand-alone/plugin/css/dispatchpf.css
@@ -118,14 +118,11 @@ ul.fancytree-container a {
 [class^="icon-"],
 [class*=" icon-"] {
   display: inline;
-  width: auto;
-  height: auto;
   line-height: normal;
   vertical-align: baseline;
   background-image: none;
   background-position: 0% 0%;
   background-repeat: repeat;
-  margin-top: 0;
 }
 [class^="icon-"],
 [class*=" icon-"] {
@@ -141,7 +138,6 @@ ul.fancytree-container a {
 [class*=" icon-"]:before {
   text-decoration: inherit;
   display: inline-block;
-  speak: none;
 }
 
 .icon-bar-chart:before {
@@ -166,7 +162,6 @@ ul.fancytree-container a {
 [class^="icon-"]:before, [class*=" icon-"]:before {
     text-decoration: inherit;
     display: inline-block;
-    speak: none;
 }
 
 #svg_legend {

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/e54c632b/console/stand-alone/plugin/html/qdrTopology.html
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/html/qdrTopology.html b/console/stand-alone/plugin/html/qdrTopology.html
index e355413..91c58a1 100644
--- a/console/stand-alone/plugin/html/qdrTopology.html
+++ b/console/stand-alone/plugin/html/qdrTopology.html
@@ -128,7 +128,7 @@ button.page-menu-button {
     opacity: 0.1;
   }
 
-  #topo_legend ul.addresses li, #topo_legend ul.options li {
+  #topo_legend ul.addresses li {
     margin-top: 0.5em;
     margin-bottom: 1em;
   }
@@ -137,6 +137,10 @@ button.page-menu-button {
   margin-bottom: 1.5em;
 }
 
+#topo_logend ul.congestion, #topo_logend ul.congestion li {
+    margin-bottom: 0;
+    padding-bottom: 0;
+}
   /* the checkboxes for the addresses */
   #topo_legend ul li input[type=checkbox]:checked + label::before {
     content:'\2713';
@@ -168,6 +172,15 @@ button.page-menu-button {
     max-height: 11.6em;   /* show up to 4 addresses */
     overflow-y: auto;
 }
+
+li.legend-sublist > ul ul {
+    margin-left: 1em;
+}
+
+#popover-div h4 {
+    margin-top: 0;
+    margin-bottom: 0;
+}
 </style>
 <div class="qdrTopology" ng-controller="QDR.TopologyController">
     <div  ng-controller="QDR.TopologyFormController">
@@ -185,22 +198,52 @@ button.page-menu-button {
 
     <div class="legend-container hidden-xs">
         <uib-accordion id="topo_legend" close-others="false">
-            <div uib-accordion-group class="panel-default" is-open="legend.status.optionsOpen" heading="Options">
+            <div uib-accordion-group class="panel-default" is-open="legend.status.optionsOpen" heading="Show Traffic">
               <ul class="options">
-                  <li class="legend-line">
-                    <checkbox title="Show message traffic" ng-model="legendOptions.showTraffic"></checkbox>
-                    <span class="legend-text" title="Select to show message traffic">Show traffic</span>
-                  </li>
                   <li class="legend-sublist" ng-hide="!legendOptions.showTraffic">
-                    <ul class="addresses">
-                        <li ng-repeat="(address, color) in addresses" class="legend-line">
-                          <checkbox style="background-color: {{addressColors[address]}};"
-                            title="{{address}}" ng-change="addressFilterChanged()"
-                            ng-model="addresses[address]"></checkbox>
-                          <span class="legend-text" ng-mouseenter="enterLegend(address)" ng-mouseleave="leaveLegend()" ng-click="addressClick(address)" title="{{address}}">{{address | limitTo : 15}}{{address.length>15 ? '…' : ''}}</span>
+                    <ul>
+                        <li><label>
+                            <input type='radio' ng-model="legendOptions.trafficType" value="dots" />
+                        Message path by address
+                        </label></li>
+                        <li>
+                            <ul class="addresses" ng-show="legendOptions.trafficType === 'dots'">
+                                    <li ng-repeat="(address, color) in addresses" class="legend-line">
+                                        <checkbox style="background-color: {{addressColors[address]}};"
+                                        title="{{address}}" ng-change="addressFilterChanged()"
+                                        ng-model="addresses[address]"></checkbox>
+                                        <span class="legend-text" ng-mouseenter="enterLegend(address)" ng-mouseleave="leaveLegend()" ng-click="addressClick(address)" title="{{address}}">{{address | limitTo : 15}}{{address.length>15 ? '…' : ''}}</span>
+                                    </li>
+                            </ul>
+                        </li>
+                    </ul>
+                    <ul>
+                        <li><label>
+                            <input type='radio' ng-model="legendOptions.trafficType" value="congestion" />
+                        Link utilization
+                        </label></li>
+                        <li>
+                            <ul class="congestion" ng-show="legendOptions.trafficType === 'congestion'">
+                                    <li>
+                                        <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" preserveAspectRatio="xMidYMid meet" width="140" height="40">
+                                            <defs>
+                                                <linearGradient xmlns="http://www.w3.org/2000/svg" id="gradienta1bEihLEHL" gradientUnits="userSpaceOnUse" x1="0%" y1="0%" x2="100%" y2="0%">
+                                                    <stop style="stop-color: #cccccc;stop-opacity: 1" offset="0"/>
+                                                    <stop style="stop-color: #cccccc;stop-opacity: 1" offset="0.06"/>
+                                                    <stop style="stop-color: #00FF00;stop-opacity: 1" offset="0.333"/>
+                                                    <stop style="stop-color: #FFA500;stop-opacity: 1" offset="0.666"/>
+                                                    <stop style="stop-color: #FF0000;stop-opacity: 1" offset="1"/></linearGradient>
+                                            </defs>
+                                            <g>
+                                                <rect width="140" height="20" x="0" y="0" fill="url(#gradienta1bEihLEHL)"></rect>
+                                                <text x="1" y="30" class="label">Idle</text>
+                                                <text x="110" y="30" class="label">Busy</text>
+                                            </g>
+                                        </svg></li>
+                            </ul>
                         </li>
                     </ul>
-                  </li>
+                </li>
               </ul>
             </div>
             <div uib-accordion-group class="panel-default" is-open="legend.status.legendOpen" heading="Legend">

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/e54c632b/console/stand-alone/plugin/js/topology/qdrTopology.js
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/js/topology/qdrTopology.js b/console/stand-alone/plugin/js/topology/qdrTopology.js
index 82af88a..1844937 100644
--- a/console/stand-alone/plugin/js/topology/qdrTopology.js
+++ b/console/stand-alone/plugin/js/topology/qdrTopology.js
@@ -98,44 +98,42 @@ var QDR = (function(QDR) {
       let nodes = [];
       let links = [];
       let forceData = {nodes: nodes, links: links};
+      let urlPrefix = $location.absUrl();
+      urlPrefix = urlPrefix.split('#')[0];
+      QDR.log.debug('started QDR.TopologyController with urlPrefix: ' + urlPrefix);
 
       $scope.multiData = [];
       $scope.quiesceState = {};
       let dontHide = false;
       $scope.crosshtml = $sce.trustAsHtml('');
-      $scope.legendOptions = angular.fromJson(localStorage[TOPOOPTIONSKEY]) || {showTraffic: false};
+      $scope.legendOptions = angular.fromJson(localStorage[TOPOOPTIONSKEY]) || {showTraffic: false, trafficType: 'dots'};
+      if (!$scope.legendOptions.trafficType)
+        $scope.legendOptions.trafficType = 'dots';
       $scope.legend = {status: {legendOpen: true, optionsOpen: true}};
+      $scope.legend.status.optionsOpen = $scope.legendOptions.showTraffic;
       let traffic = new Traffic($scope, $timeout, QDRService, separateAddresses, 
-        radius, forceData, nextHop);
+        radius, forceData, nextHop, $scope.legendOptions.trafficType, urlPrefix);
 
       // the showTraaffic checkbox was just toggled (or initialized)
-      $scope.$watch('legendOptions.showTraffic', function () {
+      $scope.$watch('legend.status.optionsOpen', function () {
+        $scope.legendOptions.showTraffic = $scope.legend.status.optionsOpen;
         localStorage[TOPOOPTIONSKEY] = JSON.stringify($scope.legendOptions);
-        if ($scope.legendOptions.showTraffic) {
+        if ($scope.legend.status.optionsOpen) {
           traffic.start();
         } else {
           traffic.stop();
           traffic.remove();
+          restart();
+        }
+      });
+      $scope.$watch('legendOptions.trafficType', function () {
+        localStorage[TOPOOPTIONSKEY] = JSON.stringify($scope.legendOptions);
+        if ($scope.legendOptions.showTraffic) {
+          restart();
+          traffic.setAnimationType($scope.legendOptions.trafficType, separateAddresses, radius);
+          traffic.start();
         }
       });
-      // event notification that an address checkbox has changed
-      $scope.addressFilterChanged = function () {
-        traffic.updateAddresses();
-      };
-
-      // called by angular when mouse enters one of the address legends
-      $scope.enterLegend = function (address) {
-        // fade all flows that aren't for this address
-        traffic.fadeOtherAddresses(address);
-      };
-      // called when the mouse leaves one of the address legends
-      $scope.leaveLegend = function () {
-        traffic.unFadeAll();
-      };
-      // clicked on the address name. toggle the address checkbox
-      $scope.addressClick = function (address) {
-        traffic.toggleAddress(address);
-      };
 
       $scope.quiesceConnection = function(row) {
         let entity = row.entity;
@@ -342,10 +340,6 @@ var QDR = (function(QDR) {
         ]
       };
 
-      let urlPrefix = $location.absUrl();
-      urlPrefix = urlPrefix.split('#')[0];
-      QDR.log.debug('started QDR.TopologyController with urlPrefix: ' + urlPrefix);
-
       // mouse event vars
       let selected_node = null,
         selected_link = null,
@@ -759,31 +753,28 @@ var QDR = (function(QDR) {
           .on('end', function () {savePositions();})
           .start();
 
-        svg.append('svg:defs').selectAll('marker')
-          .data(['end-arrow', 'end-arrow-selected', 'end-arrow-small', 'end-arrow-highlighted']) // Different link/path types can be defined here
-          .enter().append('svg:marker') // This section adds in the arrows
-          .attr('id', String)
+        // This section adds in the arrows
+        svg.append('svg:defs').attr('class', 'marker-defs').selectAll('marker')
+          .data(['end-arrow', 'end-arrow-selected', 'end-arrow-small', 'end-arrow-highlighted', 
+            'start-arrow', 'start-arrow-selected', 'start-arrow-small', 'start-arrow-highlighted'])
+          .enter().append('svg:marker') 
+          .attr('id', function (d) { return d; })
           .attr('viewBox', '0 -5 10 10')
-          .attr('refX', 24)
+          .attr('refX', function (d) { 
+            if (d.substr(0, 3) === 'end') {
+              return 24;
+            }
+            return d !== 'start-arrow-small' ? -14 : -24;})
           .attr('markerWidth', 4)
           .attr('markerHeight', 4)
           .attr('orient', 'auto')
           .classed('small', function (d) {return d.indexOf('small') > -1;})
           .append('svg:path')
-          .attr('d', 'M 0 -5 L 10 0 L 0 5 z');
-
-        svg.append('svg:defs').selectAll('marker')
-          .data(['start-arrow', 'start-arrow-selected', 'start-arrow-small', 'start-arrow-highlighted']) // Different link/path types can be defined here
-          .enter().append('svg:marker') // This section adds in the arrows
-          .attr('id', String)
-          .attr('viewBox', '0 -5 10 10')
-          .attr('refX', function (d) { return d !== 'start-arrow-small' ? -14 : -24;})
-          .attr('markerWidth', 4)
-          .attr('markerHeight', 4)
-          .attr('orient', 'auto')
-          .append('svg:path')
-          .attr('d', 'M 10 -5 L 0 0 L 10 5 z');
+          .attr('d', function (d) {
+            return d.substr(0, 3) === 'end' ? 'M 0 -5 L 10 0 L 0 5 z' : 'M 10 -5 L 0 0 L 10 5 z';
+          });
 
+        // gradient for sender/receiver client
         let grad = svg.append('svg:defs').append('linearGradient')
           .attr('id', 'half-circle')
           .attr('x1', '0%')
@@ -1127,6 +1118,8 @@ var QDR = (function(QDR) {
       // takes the nodes and links array of objects and adds svg elements for everything that hasn't already
       // been added
       function restart(start) {
+        if (!circle)
+          return;
         circle.call(force.drag);
 
         // path (link) group
@@ -1138,19 +1131,22 @@ var QDR = (function(QDR) {
         })
           .classed('highlighted', function(d) {
             return d.highlighted;
-          })
-          .attr('marker-start', function(d) {
-            let sel = d === selected_link ? '-selected' : (d.cls === 'small' ? '-small' : '');
-            if (d.highlighted)
-              sel = '-highlighted';
-            return d.left ? 'url(' + urlPrefix + '#start-arrow' + sel + ')' : '';
-          })
-          .attr('marker-end', function(d) {
-            let sel = d === selected_link ? '-selected' : (d.cls === 'small' ? '-small' : '');
-            if (d.highlighted)
-              sel = '-highlighted';
-            return d.right ? 'url(' + urlPrefix + '#end-arrow' + sel + ')' : '';
           });
+        if (!$scope.legend.status.optionsOpen || $scope.legendOptions.trafficType === 'dots') {
+          path
+            .attr('marker-start', function(d) {
+              let sel = d === selected_link ? '-selected' : (d.cls === 'small' ? '-small' : '');
+              if (d.highlighted)
+                sel = '-highlighted';
+              return d.left ? 'url(' + urlPrefix + '#start-arrow' + sel + ')' : '';
+            })
+            .attr('marker-end', function(d) {
+              let sel = d === selected_link ? '-selected' : (d.cls === 'small' ? '-small' : '');
+              if (d.highlighted)
+                sel = '-highlighted';
+              return d.right ? 'url(' + urlPrefix + '#end-arrow' + sel + ')' : '';
+            });
+        }
         // add new links. if a link with a new uid is found in the data, add a new path
         path.enter().append('svg:path')
           .attr('class', 'link')
@@ -1198,16 +1194,27 @@ var QDR = (function(QDR) {
             restart();
           })
           .on('mousemove', function (d) {
+            let updateTooltip = function () {
+              $timeout(function () {
+                $scope.trustedpopoverContent = $sce.trustAsHtml(connectionPopupHTML(d));
+              });
+            };
+            // update the contents of the popup tooltip each time the data is polled
+            QDRService.management.topology.addUpdatedAction('connectionPopupHTML', updateTooltip);
+
+            // show the tooltip
             let top = $('#topology').offset().top - 5;
-            $timeout(function () {
-              $scope.trustedpopoverContent = $sce.trustAsHtml(connectionPopupHTML(d));
-            });
             d3.select('#popover-div')
               .style('display', 'block')
               .style('left', (d3.event.pageX+5)+'px')
               .style('top', (d3.event.pageY-top)+'px');
+
+            // update the tooltip right now
+            updateTooltip();
+
           })
           .on('mouseout', function() { // mouse out of a path
+            QDRService.management.topology.delUpdatedAction('connectionPopupHTML');
             d3.select('#popover-div')
               .style('display', 'none');
             selected_link = null;
@@ -1993,6 +2000,7 @@ var QDR = (function(QDR) {
         QDRService.management.topology.stopUpdating();
         QDRService.management.topology.delUpdatedAction('normalsStats');
         QDRService.management.topology.delUpdatedAction('topology');
+        QDRService.management.topology.delUpdatedAction('connectionPopupHTML');
 
         d3.select('#SVG_ID').remove();
         window.removeEventListener('resize', resize);
@@ -2094,16 +2102,23 @@ var QDR = (function(QDR) {
         });
         if (rightIndex < 0) {
           // we have a connection to a client/service
-          rightIndex = +left.connectionId;
+          rightIndex = +left.resultIndex;
         }
         if (isNaN(rightIndex)) {
           // we have a connection to a console
-          rightIndex = +right.connectionId;
+          rightIndex = +right.resultIndex;
         }
         let HTML = '';
         if (rightIndex >= 0) {
           let conn = onode['connection'].results[rightIndex];
           conn = QDRService.utilities.flatten(onode['connection'].attributeNames, conn);
+          if ($scope.legend.status.optionsOpen && traffic) {
+            HTML = traffic.connectionPopupHTML(onode, conn, d);
+            if (HTML)
+              return HTML;
+            else
+              HTML = '';
+          }
           HTML += '<table class="popupTable">';
           HTML += ('<tr><td>Security</td><td>' + connSecurity(conn) + '</td></tr>');
           HTML += ('<tr><td>Authentication</td><td>' + connAuth(conn) + '</td></tr>');
@@ -2122,4 +2137,4 @@ var QDR = (function(QDR) {
 
   return QDR;
 
-}(QDR || {}));
\ No newline at end of file
+}(QDR || {}));

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/e54c632b/console/stand-alone/plugin/js/topology/traffic.js
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/js/topology/traffic.js b/console/stand-alone/plugin/js/topology/traffic.js
index 883cb2f..e061000 100644
--- a/console/stand-alone/plugin/js/topology/traffic.js
+++ b/console/stand-alone/plugin/js/topology/traffic.js
@@ -18,42 +18,25 @@ under the License.
 */
 'use strict';
 
-/* global d3 ChordData MIN_CHORD_THRESHOLD */
+/* global d3 ChordData MIN_CHORD_THRESHOLD Promise */
 
-/* Create animated dots moving along the links between routers
-   to show that there is message flow between routers.
-   */
 const transitionDuration = 1000;
-const CHORDFILTERKEY =  'chordFilter';
+const CHORDFILTERKEY = 'chordFilter';
 
-function Traffic ($scope, $timeout, QDRService, converter, radius, topology, nextHop, excluded) {
+function Traffic ($scope, $timeout, QDRService, converter, radius, topology, nextHop, type, prefix) {
+  $scope.addressColors = {};
   this.QDRService = QDRService;
-  this.radius = radius;       // the radius of a router circle
-  this.topology = topology;   // contains the list of router nodes
-  this.nextHop = nextHop;     // fn that returns the route through the network between two routers
+  this.type = type; // moving dots or colored path
+  this.prefix = prefix; // url prefix used in svg url()s
+  this.topology = topology; // contains the list of router nodes
+  this.nextHop = nextHop; // fn that returns the route through the network between two routers
   this.$scope = $scope;
   this.$timeout = $timeout;
-  $scope.addressColors = {};
-  this.excludedAddresses = JSON.parse(localStorage[CHORDFILTERKEY]) || [];
-
   // internal variables
-  this.interval = null;       // setInterval handle
-  this.lastFlows = {};        // the number of dots animated between routers
-  this.chordData = new ChordData(this.QDRService, true, converter); // gets ingressHistogram data
-  this.chordData.setFilter(this.excludedAddresses);
-  this.$scope.addresses = {};
-  this.chordData.getMatrix().then( function () {
-    $timeout( function () {
-      this.$scope.addresses = this.chordData.getAddresses();
-      for (let address in this.$scope.addresses) {
-        this.fillColor(address);
-      }
-    }.bind(this));
-  }.bind(this));
+  this.interval = null; // setInterval handle
+  this.setAnimationType(type, converter, radius);
 }
 
-/* Public methods on Traffic object */
-
 // stop updating the traffic data
 Traffic.prototype.stop = function () {
   if (this.interval) {
@@ -63,124 +46,384 @@ Traffic.prototype.stop = function () {
 };
 // start updating the traffic data
 Traffic.prototype.start = function () {
-  this.doUpdate(this);
+  this.doUpdate();
   this.interval = setInterval(this.doUpdate.bind(this), transitionDuration);
 };
-// remove any animationions that are in progress
-Traffic.prototype.remove = function () {
+// remove any animations that are in progress
+Traffic.prototype.remove = function() {
+  if (this.vis)
+    this.vis.remove();
+};
+// called when one of the address checkboxes is toggled
+Traffic.prototype.setAnimationType = function (type, converter, radius) {
+  this.stop();
+  this.remove();
+  this.type = type;
+  this.vis = type === 'dots' ? new Dots(this, converter, radius) :
+    new Congestion(this);
+};
+// called periodically to refresh the traffic flow
+Traffic.prototype.doUpdate = function () {
+  this.vis.doUpdate();
+};
+Traffic.prototype.connectionPopupHTML = function (onode, conn, d) {
+  return this.vis.connectionPopupHTML(onode, conn, d);
+};
+
+/* Base class for congestion and dots visualizations */
+function TrafficAnimation (traffic) {
+  this.traffic = traffic;
+}
+TrafficAnimation.prototype.nodeIndexFor = function (nodes, name) {
+  for (let i=0; i<nodes.length; i++) {
+    let node = nodes[i];
+    if (node.container === name)
+      return i;
+  }
+  return -1;
+};
+TrafficAnimation.prototype.connectionPopupHTML = function () {
+  return null;
+};
+
+/* Color the links between router to show how heavily used the links are. */
+function Congestion (traffic) {
+  TrafficAnimation.call(this, traffic);
+  this.init_markerDef();
+}
+Congestion.prototype = Object.create(TrafficAnimation.prototype);
+Congestion.prototype.constructor = Congestion;
+
+Congestion.prototype.init_markerDef = function () {
+  this.custom_markers_def = d3.select('#SVG_ID').select('defs.custom-markers');
+  if (this.custom_markers_def.empty()) {
+    this.custom_markers_def = d3.select('#SVG_ID').append('svg:defs').attr('class', 'custom-markers');
+  }
+};
+Congestion.prototype.findResult = function (node, entity, attribute, value) {
+  let attrIndex = node[entity].attributeNames.indexOf(attribute);
+  if (attrIndex >= 0) {
+    for (let i=0; i<node[entity].results.length; i++) {
+      if (node[entity].results[i][attrIndex] === value) {
+        return this.traffic.QDRService.utilities.flatten(node[entity].attributeNames, node[entity].results[i]);
+      }
+    }
+  }
+  return null;
+};
+Congestion.prototype.doUpdate = function () {
+  let self = this;
+  this.traffic.QDRService.management.topology.ensureAllEntities(
+    [{ entity: 'router.link', force: true},{entity: 'connection'}], function () {
+      let links = {};
+      let nodeInfo = self.traffic.QDRService.management.topology.nodeInfo();
+      // accumulate all the inter-router links in an object
+      // keyed by the svgs path id
+      for (let nodeId in nodeInfo) {
+        let node = nodeInfo[nodeId];
+        let nodeLinks = node['router.link'];
+        for (let n=0; n<nodeLinks.results.length; n++) {
+          let link = self.traffic.QDRService.utilities.flatten(nodeLinks.attributeNames, nodeLinks.results[n]);
+          if (link.linkType !== 'router-control') {
+            let f = self.nodeIndexFor(self.traffic.topology.nodes, 
+              self.traffic.QDRService.management.topology.nameFromId(nodeId));
+            let connection = self.findResult(node, 'connection', 'identity', link.connectionId);
+            if (connection) {
+              let t = self.nodeIndexFor(self.traffic.topology.nodes, connection.container);
+              let little = Math.min(f, t);
+              let big = Math.max(f, t);
+              let key = ['#path', little, big].join('-');
+              if (!links[key])
+                links[key] = [];
+              links[key].push(link);
+            }
+          }
+        }
+      }
+      // accumulate the colors/directions to be used
+      let colors = {};
+      for (let key in links) {
+        let congestion = self.congestion(links[key]);
+        let path = d3.select(key);
+        if (path && !path.empty()) {
+          let dir = path.attr('marker-end') === '' ? 'start' : 'end';
+          let small = path.attr('class').indexOf('small') > -1;
+          let id = dir + '-' + congestion.substr(1) + (small ? '-s' : '');
+          colors[id] = {dir: dir, color: congestion, small: small};
+          path
+            .attr('stroke', congestion)
+            .classed('traffic', true)
+            .attr('marker-start', function(d) {
+              return d.left ? 'url(' + self.traffic.prefix + '#' + id + ')' : '';
+            })
+            .attr('marker-end', function(d) {
+              return d.right ? 'url(' + self.traffic.prefix + '#' + id + ')' : '';
+            });
+        }
+      }
+
+      // create the svg:def that holds the custom markers
+      self.init_markerDef();
+
+      let colorKeys = Object.keys(colors);
+      let custom_markers = self.custom_markers_def.selectAll('marker')
+        .data(colorKeys, function (d) {return d;});
+
+      custom_markers.enter().append('svg:marker')
+        .attr('id', function (d) { return d; })
+        .attr('viewBox', '0 -5 10 10')
+        .attr('refX', function (d) {
+          return colors[d].dir === 'end' ? 24 : (colors[d].small) ? -24 : -14;
+        })
+        .attr('markerWidth', 4)
+        .attr('markerHeight', 4)
+        .attr('orient', 'auto')
+        .style('fill', function (d) {return colors[d].color;})
+        .append('svg:path')
+        .attr('d', function (d) {
+          return colors[d].dir === 'end' ? 'M 0 -5 L 10 0 L 0 5 z' : 'M 10 -5 L 0 0 L 10 5 z';
+        });
+      custom_markers.exit().remove();
+
+    });
+};
+Congestion.prototype.congestion = function (links) {
+  let v = 0;
+  for (let l=0; l<links.length; l++) {
+    let link = links[l];
+    v = Math.max(v, (link.undeliveredCount+link.unsettledCount)/link.capacity);
+  }
+  return this.fillColor(v);
+};
+Congestion.prototype.fillColor = function (v) {
+  let color = d3.scale.linear().domain([0, 1, 2, 3])
+    .interpolate(d3.interpolateHcl)
+    .range([d3.rgb('#CCCCCC'), d3.rgb('#00FF00'), d3.rgb('#FFA500'), d3.rgb('#FF0000')]);
+  return color(v);
+};
+Congestion.prototype.remove = function () {
+  d3.select('#SVG_ID').selectAll('path.traffic')
+    .classed('traffic', false);
+  d3.select('#SVG_ID').select('defs.custom-markers')
+    .selectAll('marker').remove();
+};
+
+// construct HTML to be used in a popup when the mouse is moved over a link.
+// The HTML is sanitized elsewhere before it is displayed
+Congestion.prototype.connectionPopupHTML = function (onode, conn, d) {
+  const max_links = 10;
+  const fields = ['undelivered', 'unsettled', 'rejected', 'released', 'modified'];
+  // local function to determine if a link's connectionId is in any of the connections
+  let isLinkFor = function (connectionId, conns) {
+    for (let c=0; c<conns.length; c++) {
+      if (conns[c].identity === connectionId)
+        return true;
+    }
+    return false;
+  };
+  let fnJoin = function (ar, sepfn) {
+    let out = '';
+    out = ar[0];
+    for (let i=1; i<ar.length; i++) {
+      let sep = sepfn(ar[i]);
+      out += (sep[0] + sep[1]);
+    }
+    return out;
+  };
+  let conns = [conn];
+  // if the data for the line is from a client (small circle), we may have multiple connections
+  if (d.cls === 'small') {
+    conns = [];
+    let normals = d.target.normals ? d.target.normals : d.source.normals;
+    for (let n=0; n<normals.length; n++) {
+      if (normals[n].resultIndex !== undefined) {
+        conns.push(this.traffic.QDRService.utilities.flatten(onode['connection'].attributeNames,
+          onode['connection'].results[normals[n].resultIndex]));
+      }
+    }
+  }
+  // loop through all links for this router and accumulate those belonging to the connection(s)
+  let nodeLinks = onode['router.link'];
+  let links = [];
+  let hasAddress = false;
+  for (let n=0; n<nodeLinks.results.length; n++) {
+    let link = this.traffic.QDRService.utilities.flatten(nodeLinks.attributeNames, nodeLinks.results[n]);
+    if (link.linkType !== 'router-control') {
+      if (isLinkFor(link.connectionId, conns)) {
+        if (link.owningAddr)
+          hasAddress = true;
+        links.push(link);
+      }
+    }
+  }
+  // we may need to limit the number of links displayed, so sort descending by the sum of the field values
+  links.sort( function (a, b) {
+    let asum = a.undeliveredCount + a.unsettledCount + a.rejectedCount + a.releasedCount + a.modifiedCount;
+    let bsum = b.undeliveredCount + b.unsettledCount + b.rejectedCount + b.releasedCount + b.modifiedCount;
+    return asum < bsum ? 1 : asum > bsum ? -1 : 0;
+  });
+  let HTMLHeading = '<h4>Links</h4>';
+  let HTML = '<table class="popupTable">';
+  // copy of fields since we may be prepending an address
+  let th = fields.slice();
+  // convert to actual attribute names
+  let td = fields.map( function (f) {return f + 'Count';});
+  th.unshift('dir');
+  td.unshift('linkDir');
+  // add an address field if any of the links had an owningAddress
+  if (hasAddress) {
+    th.unshift('address');
+    td.unshift('owningAddr');
+  }
+  // add rows to the table for each link
+  HTML += ('<tr><td>' + th.join('</td><td>') + '</tr></td>');
+  for (let l=0; l<links.length; l++) {
+    if (l>=max_links) {
+      HTMLHeading = '<h4>Top ' + max_links + ' Links</h4>';
+      break;
+    }
+    let link = links[l];
+    let vals = td.map( function (f) {
+      if (f === 'owningAddr') {
+        let identity = this.traffic.QDRService.utilities.identity_clean(link.owningAddr);
+        return this.traffic.QDRService.utilities.addr_text(identity);
+      }
+      return link[f];
+    }.bind(this));
+    let joinedVals = fnJoin(vals, function (v1) {
+      return ['</td><td' + (isNaN(+v1) ? '': ' align="right"') + '>', this.traffic.QDRService.utilities.pretty(v1 || '0')];
+    }.bind(this));
+    HTML += ('<tr><td>' + joinedVals + '</td></tr>');
+  }
+  HTML += '</table>';
+  return HTMLHeading + HTML;
+};
+
+/* Create animated dots moving along the links between routers
+   to show message flow */
+function Dots (traffic, converter, radius) {
+  TrafficAnimation.call(this, traffic);
+  this.excludedAddresses = localStorage[CHORDFILTERKEY] ? JSON.parse(localStorage[CHORDFILTERKEY]) : [];
+  this.radius = radius; // the radius of a router circle
+  this.lastFlows = {}; // the number of dots animated between routers
+  this.chordData = new ChordData(this.traffic.QDRService, true, converter); // gets ingressHistogram data
+  this.chordData.setFilter(this.excludedAddresses);
+  traffic.$scope.addresses = {};
+  this.chordData.getMatrix().then(function () {
+    this.traffic.$timeout(function () {
+      this.traffic.$scope.addresses = this.chordData.getAddresses();
+      for (let address in this.traffic.$scope.addresses) {
+        this.fillColor(address);
+      }
+    }.bind(this));
+  }.bind(this));
+  // colors
+  this.colorGen = d3.scale.category10();
+  let self = this;
+  // event notification that an address checkbox has changed
+  traffic.$scope.addressFilterChanged = function () {
+    self.updateAddresses()
+      .then(function () {
+      // don't wait for the next polling cycle. update now
+        self.traffic.stop();
+        self.traffic.start();
+      });
+  };
+  // called by angular when mouse enters one of the address legends
+  traffic.$scope.enterLegend = function (address) {
+    // fade all flows that aren't for this address
+    self.fadeOtherAddresses(address);
+  };
+  // called when the mouse leaves one of the address legends
+  traffic.$scope.leaveLegend = function () {
+    self.unFadeAll();
+  };
+  // clicked on the address name. toggle the address checkbox
+  traffic.$scope.addressClick = function (address) {
+    self.toggleAddress(address)
+      .then(function () {
+        self.updateAddresses();
+      });
+  };
+}
+Dots.prototype = Object.create(TrafficAnimation.prototype);
+Dots.prototype.constructor = Dots;
+
+Dots.prototype.remove = function () {
   for (let id in this.lastFlows) {
     d3.select('#SVG_ID').selectAll('circle.flow' + id).remove();
   }
   this.lastFlows = {};
 };
-// called when one of the address checkboxes is toggled
-Traffic.prototype.updateAddresses = function () {
+Dots.prototype.updateAddresses = function () {
   this.excludedAddresses = [];
-  for (let address in this.$scope.addresses) {
-    if (!this.$scope.addresses[address])
+  for (let address in this.traffic.$scope.addresses) {
+    if (!this.traffic.$scope.addresses[address])
       this.excludedAddresses.push(address);
   }
   localStorage[CHORDFILTERKEY] = JSON.stringify(this.excludedAddresses);
-  if (this.chordData) 
+  if (this.chordData)
     this.chordData.setFilter(this.excludedAddresses);
-  // don't wait for the next polling cycle. update now
-  this.stop();
-  this.start();
+  return new Promise(function (resolve) {
+    return resolve();
+  });
 };
-Traffic.prototype.toggleAddress = function (address) {
-  this.$scope.addresses[address] = !this.$scope.addresses[address];
-  this.updateAddresses();
+Dots.prototype.toggleAddress = function (address) {
+  this.traffic.$scope.addresses[address] = !this.traffic.$scope.addresses[address];
+  return new Promise(function (resolve) {
+    return resolve();
+  });
 };
-Traffic.prototype.fadeOtherAddresses = function (address) {
-  d3.selectAll('circle.flow').classed('fade', function(d) {
+Dots.prototype.fadeOtherAddresses = function (address) {
+  d3.selectAll('circle.flow').classed('fade', function (d) {
     return d.address !== address;
   });
 };
-Traffic.prototype.unFadeAll = function () {
+Dots.prototype.unFadeAll = function () {
   d3.selectAll('circle.flow').classed('fade', false);
 };
-
-/* The following don't need to be public, but they are for simplicity sake */
-
-// called periodically to refresh the traffic flow
-Traffic.prototype.doUpdate = function () {
+Dots.prototype.doUpdate = function () {
   let self = this;
   // we need the nextHop data to show traffic between routers that are connected by intermediaries
-  this.QDRService.management.topology.ensureAllEntities([{entity: 'router.node', attrs: ['id','nextHop']}], 
-    function () {
-      // get the ingressHistogram data for all routers
-      self.chordData.getMatrix().then(self.render.bind(self), function (e) {
-        console.log('Could not get message histogram' + e);
-      });
+  this.traffic.QDRService.management.topology.ensureAllEntities([{ entity: 'router.node', attrs: ['id', 'nextHop'] }], function () {
+    // get the ingressHistogram data for all routers
+    self.chordData.getMatrix().then(self.render.bind(self), function (e) {
+      console.log('Could not get message histogram' + e);
     });
+  });
 };
-
-// calculate the translation for each dot along the path
-let translateDots = function (radius, path, count, back) {
-  let pnode = path.node();
-  // will be called for each element in the flow selection (for each dot)
-  return function(d) {
-    // will be called with t going from 0 to 1 for each dot
-    return function(t) {
-      // start the points at different positions depending on their value (d)
-      let tt = t * 1000;
-      let f = ((tt + (d.i*1000/count)) % 1000)/1000;
-      if (back)
-        f = 1 - f;
-      // l needs to be calculated each tick because the path's length might be changed during the animation
-      let l = pnode.getTotalLength();
-      let p = pnode.getPointAtLength(f * l);
-      return 'translate(' + p.x + ',' + p.y + ')';
-    };
-  };
-};
-// animate the d3 selection (flow) along the given path
-Traffic.prototype.animateFlow = function (flow, path, count, back, rate) {
-  let self = this;
-  let l = path.node().getTotalLength();
-  flow.transition()
-    .ease('easeLinear')
-    .duration(l*10/rate)
-    .attrTween('transform', translateDots(self.radius, path, count, back))
-    .each('end', function () {self.animateFlow(flow, path, count, back, rate);});
-};
-
-Traffic.prototype.render = function (matrix) {
-  this.$timeout(
-    function () {
-      this.$scope.addresses = this.chordData.getAddresses();
-    }.bind(this)
-  );
+Dots.prototype.render = function (matrix) {
+  this.traffic.$timeout(function () {
+    this.traffic.$scope.addresses = this.chordData.getAddresses();
+  }.bind(this));
   // get the rate of message flow between routers
-  let hops = {};  // every hop between routers that is involved in message flow
+  let hops = {}; // every hop between routers that is involved in message flow
   let matrixMessages = matrix.matrixMessages();
   // the fastest traffic rate gets 3 times as many dots as the slowest
   let minmax = matrix.getMinMax();
-  let flowScale = d3.scale.linear().domain(minmax).range([1,1.75]);
-
+  let flowScale = d3.scale.linear().domain(minmax).range([1, 1.75]);
   // row is ingress router, col is egress router. Value at [row][col] is the rate
-  matrixMessages.forEach( function (row, r) {
+  matrixMessages.forEach(function (row, r) {
     row.forEach(function (val, c) {
       if (val > MIN_CHORD_THRESHOLD) {
         // translate between matrix row/col and node index
-        let f = nodeIndexFor(this.topology.nodes, matrix.rows[r].egress);
-        let t = nodeIndexFor(this.topology.nodes, matrix.rows[r].ingress);
+        let f = this.nodeIndexFor(this.traffic.topology.nodes, matrix.rows[r].egress);
+        let t = this.nodeIndexFor(this.traffic.topology.nodes, matrix.rows[r].ingress);
         let address = matrix.getAddress(r, c);
-
         if (r !== c) {
-          // move the dots along the links between the routers
-          this.nextHop(this.topology.nodes[f], this.topology.nodes[t], function (link, fnode, tnode) {
+          // accumulate the hops between the ingress and egress routers
+          this.traffic.nextHop(this.traffic.topology.nodes[f], this.traffic.topology.nodes[t], function (link, fnode, tnode) {
             let key = '-' + link.uid;
             let back = fnode.index < tnode.index;
             if (!hops[key])
               hops[key] = [];
-            hops[key].push({val: val, back: back, address: address});
+            hops[key].push({ val: val, back: back, address: address });
           });
         }
         // Find the senders connected to nodes[f] and the receivers connected to nodes[t]
         // and add their links to the animation
-        addClients(hops, this.topology.nodes, f, val, true, address);
-        addClients(hops, this.topology.nodes, t, val, false, address);
+        this.addClients(hops, this.traffic.topology.nodes, f, val, true, address);
+        this.addClients(hops, this.traffic.topology.nodes, t, val, false, address);
       }
     }.bind(this));
   }.bind(this));
@@ -188,12 +431,12 @@ Traffic.prototype.render = function (matrix) {
   let keep = {};
   for (let id in hops) {
     let hop = hops[id];
-    for (let h=0; h<hop.length; h++) {
+    for (let h = 0; h < hop.length; h++) {
       let ahop = hop[h];
-      let flowId = id + '-' + addressIndex(this, ahop.address) + (ahop.back ? 'b' : '');
+      let flowId = id + '-' + this.addressIndex(this, ahop.address) + (ahop.back ? 'b' : '');
       let path = d3.select('#path' + id);
       // start the animation. If the animation is already running, this will have no effect
-      this.startDots(path, flowId, ahop, flowScale(ahop.val));
+      this.startAnimation(path, flowId, ahop, flowScale(ahop.val));
       keep[flowId] = true;
     }
   }
@@ -205,17 +448,29 @@ Traffic.prototype.render = function (matrix) {
     }
   }
 };
-
+// animate the d3 selection (flow) along the given path
+Dots.prototype.animateFlow = function (flow, path, count, back, rate) {
+  let self = this;
+  let l = path.node().getTotalLength();
+  flow.transition()
+    .ease('easeLinear')
+    .duration(l * 10 / rate)
+    .attrTween('transform', self.translateDots(self.radius, path, count, back))
+    .each('end', function () { self.animateFlow(flow, path, count, back, rate); });
+};
 // create dots along the path between routers
-Traffic.prototype.startDots = function (path, id, hop, rate) {
-  let back = hop.back, address = hop.address;
+Dots.prototype.startAnimation = function (path, id, hop, rate) {
   if (!path.node())
     return;
+  this.animateDots(path, id, hop, rate);
+};
+Dots.prototype.animateDots = function (path, id, hop, rate) {
+  let back = hop.back, address = hop.address;
   // the density of dots is determined by the rate of this traffic relative to the other traffic
   let len = Math.max(Math.floor(path.node().getTotalLength() / 50), 1);
   let dots = [];
-  for (let i=0, offset=addressIndex(this, address); i<len; ++i) {
-    dots[i] = {i: i + 10 * offset, address: address};
+  for (let i = 0, offset = this.addressIndex(this, address); i < len; ++i) {
+    dots[i] = { i: i + 10 * offset, address: address };
   }
   // keep track of the number of dots for each link. If the length of the link is changed,
   // re-create the animation
@@ -229,38 +484,23 @@ Traffic.prototype.startDots = function (path, id, hop, rate) {
   }
   let flow = d3.select('#SVG_ID').selectAll('circle.flow' + id)
     .data(dots, function (d) { return d.i + d.address; });
-
   let circles = flow
     .enter().append('circle')
     .attr('class', 'flow flow' + id)
     .attr('fill', this.fillColor(address))
     .attr('r', 5);
-
   this.animateFlow(circles, path, dots.length, back, rate);
-
   flow.exit()
     .remove();
 };
-
-// colors
-let colorGen = d3.scale.category10();
-Traffic.prototype.fillColor = function (n) {
-  if (!(n in this.$scope.addressColors)) {
-    let ci = Object.keys(this.$scope.addressColors).length;
-    this.$scope.addressColors[n] = colorGen(ci);
+Dots.prototype.fillColor = function (n) {
+  if (!(n in this.traffic.$scope.addressColors)) {
+    let ci = Object.keys(this.traffic.$scope.addressColors).length;
+    this.traffic.$scope.addressColors[n] = this.colorGen(ci);
   }
-  return this.$scope.addressColors[n];
+  return this.traffic.$scope.addressColors[n];
 };
-// return the node index for a router name
-let nodeIndexFor = function (nodes, name) {
-  for (let i=0; i<nodes.length; i++) {
-    let node = nodes[i];
-    if (node.routerId === name)
-      return i;
-  }
-  return -1;
-};
-let addClients = function (hops, nodes, f, val, sender, address) {
+Dots.prototype.addClients = function (hops, nodes, f, val, sender, address) {
   let cdir = sender ? 'out' : 'in';
   for (let n=0; n<nodes.length; n++) {
     let node = nodes[n];
@@ -273,6 +513,25 @@ let addClients = function (hops, nodes, f, val, sender, address) {
     }
   }
 };
-let addressIndex = function (traffic, address) {
-  return Object.keys(traffic.$scope.addresses).indexOf(address);
-};
\ No newline at end of file
+Dots.prototype.addressIndex = function (vis, address) {
+  return Object.keys(vis.traffic.$scope.addresses).indexOf(address);
+};
+// calculate the translation for each dot along the path
+Dots.prototype.translateDots = function (radius, path, count, back) {
+  let pnode = path.node();
+  // will be called for each element in the flow selection (for each dot)
+  return function(d) {
+    // will be called with t going from 0 to 1 for each dot
+    return function(t) {
+      // start the points at different positions depending on their value (d)
+      let tt = t * 1000;
+      let f = ((tt + (d.i*1000/count)) % 1000)/1000;
+      if (back)
+        f = 1 - f;
+      // l needs to be calculated each tick because the path's length might be changed during the animation
+      let l = pnode.getTotalLength();
+      let p = pnode.getPointAtLength(f * l);
+      return 'translate(' + p.x + ',' + p.y + ')';
+    };
+  };
+};


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@qpid.apache.org
For additional commands, e-mail: commits-help@qpid.apache.org