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/04/18 19:33:20 UTC

qpid-dispatch git commit: DISPATCH-970 Added Message flow page to console

Repository: qpid-dispatch
Updated Branches:
  refs/heads/master a6a4445cd -> 55e08784d


DISPATCH-970 Added Message flow page to console


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

Branch: refs/heads/master
Commit: 55e08784d489d6302b8e4242f22213046c469f80
Parents: a6a4445
Author: Ernest Allen <ea...@redhat.com>
Authored: Wed Apr 18 15:32:59 2018 -0400
Committer: Ernest Allen <ea...@redhat.com>
Committed: Wed Apr 18 15:32:59 2018 -0400

----------------------------------------------------------------------
 console/stand-alone/index.html                  |  12 +-
 console/stand-alone/package.json                |   2 +
 console/stand-alone/plugin/html/qdrChord.html   | 218 +++++
 console/stand-alone/plugin/js/chord/data.js     | 228 +++++
 console/stand-alone/plugin/js/chord/filters.js  | 107 +++
 .../plugin/js/chord/layout/README.md            |  66 ++
 .../plugin/js/chord/layout/layout.js            | 147 ++++
 console/stand-alone/plugin/js/chord/matrix.js   | 199 +++++
 console/stand-alone/plugin/js/chord/qdrChord.js | 823 +++++++++++++++++++
 .../plugin/js/chord/ribbon/README.md            |  22 +
 .../plugin/js/chord/ribbon/ribbon.js            | 165 ++++
 console/stand-alone/plugin/js/dispatchPlugin.js |   5 +-
 console/stand-alone/plugin/js/navbar.js         |   7 +
 13 files changed, 1996 insertions(+), 5 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/55e08784/console/stand-alone/index.html
----------------------------------------------------------------------
diff --git a/console/stand-alone/index.html b/console/stand-alone/index.html
index 17768cb..d3deb00 100644
--- a/console/stand-alone/index.html
+++ b/console/stand-alone/index.html
@@ -119,12 +119,14 @@ under the License.
 <script src='node_modules/d3-queue/build/d3-queue.min.js'></script>
 <script src='node_modules/d3-time/build/d3-time.min.js'></script>
 <script src='node_modules/d3-time-format/build/d3-time-format.min.js'></script>
+<script src='node_modules/d3-path/build/d3-path.min.js'></script>
 
 <!-- c3 for charts -->
 <script src="node_modules/c3/c3.js"></script>
 
 <script src="node_modules/angular-ui-slider/src/slider.js"></script>
 <script src="node_modules/angular-ui-grid/ui-grid.js"></script>
+<script src="node_modules/angular-bootstrap-checkbox/angular-bootstrap-checkbox.js"></script>
 <script src="node_modules/notifyjs-browser/dist/notify.js"></script>
 <script src="node_modules/patternfly/dist/js/patternfly.min.js"></script>
 
@@ -145,10 +147,12 @@ under the License.
 <script type="text/javascript" src="node_modules/dispatch-console-pages/dist/js/qdrChartService.js"></script>
 <script type="text/javascript" src="node_modules/dispatch-console-pages/dist/js/qdrTopology.js"></script>
 <script type="text/javascript" src="node_modules/dispatch-console-pages/dist/js/qdrSettings.js"></script>
-
-<script type="text/javascript">
-        //angular.element(document.getElementsByTagName('head')).append(angular.element('<base href="' + window.location.pathname + '" />'));
-  </script>
+<script type="text/javascript" src="plugin/js/chord/ribbon/ribbon.js"></script>
+<script type="text/javascript" src="plugin/js/chord/matrix.js"></script>
+<script type="text/javascript" src="plugin/js/chord/filters.js"></script>
+<script type="text/javascript" src="plugin/js/chord/data.js"></script>
+<script type="text/javascript" src="plugin/js/chord/layout/layout.js"></script>
+<script type="text/javascript" src="plugin/js/chord/qdrChord.js"></script>
 
 </body>
 </html>

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/55e08784/console/stand-alone/package.json
----------------------------------------------------------------------
diff --git a/console/stand-alone/package.json b/console/stand-alone/package.json
index aedaaf6..ac32845 100644
--- a/console/stand-alone/package.json
+++ b/console/stand-alone/package.json
@@ -24,6 +24,7 @@
   "dependencies": {
     "angular": "1.5.11",
     "angular-animate": "1.5.11",
+    "angular-bootstrap-checkbox": "^0.5.0",
     "angular-resource": "1.5.11",
     "angular-route": "1.5.11",
     "angular-sanitize": "1.5.11",
@@ -34,6 +35,7 @@
     "bootstrap": "^3.3.7",
     "c3": "^0.4.18",
     "d3": "^3.5.14",
+    "d3-path": "^1.0.5",
     "d3-queue": "^3.0.7",
     "d3-time-format": "^2.1.1",
     "dispatch-console-pages": "~0.1.6",

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/55e08784/console/stand-alone/plugin/html/qdrChord.html
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/html/qdrChord.html b/console/stand-alone/plugin/html/qdrChord.html
new file mode 100644
index 0000000..a1b055a
--- /dev/null
+++ b/console/stand-alone/plugin/html/qdrChord.html
@@ -0,0 +1,218 @@
+<style>
+  path.chord {
+    fill-opacity: .67;
+  }
+  #circle circle {
+    fill: none;
+    pointer-events: all;
+  }
+  path.fade {
+    opacity: 0.1;
+  }
+ 
+  .routers rect {
+    fill: white;
+  }
+  
+  g.arc text {
+    fill: black;
+    stroke-width: 0;
+  }
+  #chord {
+    position: absolute;
+  }
+
+  #switches {
+    position: absolute;
+    top: 1em;
+    margin-right: 1em;
+    padding-right: 1em;
+    opacity: 0;
+  }
+  #switches ul {
+    list-style: none;
+  }
+
+  #switches li {
+    margin-bottom: 1em;
+  }
+  #legend {
+    position: absolute;
+    right: 1em;
+    top: 1em;
+    padding: 0 1em;
+    border: 1px solid black;
+    border-radius: 4px;
+    min-width: 10em;
+    background-color: white;
+  }
+  #legend .legend-color {
+    width: 19.6667px;
+    height: 18.1667px;
+    display: inline-block;
+  }
+  #legend .legend-address {
+    white-space: nowrap;
+    padding: 0.1em 0.5em 0.2em;
+    position: absolute;
+    left: 10px;
+    top: -5px;
+  }
+
+  #legend .legend-router {
+    white-space: nowrap;
+    position: relative;
+    top: -5px;
+  }
+
+  #legend .legend-line {
+    margin: 0.5em 1em;
+    border: 1px solid white;
+  }
+  #legend .legend-line:hover {
+    border: 1px dashed lightslategray;
+  }
+  #legend ul {
+    list-style: none;
+    padding-left: 0;
+    margin-bottom: 0.5em;
+  }
+  #legend ul li label input[type='checkbox'] {
+    position: relative;
+    top: 2px;
+    padding-right: 1em;
+  }
+  #legend ul li label {
+    margin-bottom: 0;
+    font-weight: normal;
+  }
+  /* the checkboxes for the addresses */
+  #legend ul li input[type=checkbox]:checked + label::before {
+    content:'\2713';
+    font-weight: bold;
+    font-size: 16px;
+    display:inline-block;
+    /* padding:0 6px 0 0; */
+    color: black;
+    position: absolute;
+    top: -8px;
+    left: -1px;
+  }
+  /* The aggregate addresses need a black checkbox on the white background */
+  #legend ul li input[type=checkbox]:checked + label.aggregate::before {
+    color: black;
+    /* left: 1px; */
+  }
+  .bootstrap-switch {
+      border: 0px;
+  }
+  .bootstrap-switch.bootstrap-switch-small {
+      min-width: 72px;
+  }
+  .legend-address {
+    color: black;
+  }
+
+  .arrows path {
+    stroke: black;
+    opacity: 0.25;
+    stroke-width: 3;
+  }
+
+  path.empty {
+    fill: rgb(31, 119, 180);
+    fill-opacity: .67;
+  }
+
+#legend ul.addresses li, #legend ul.routers li {
+  margin-top: 0.5em;
+}
+
+#legend ul.routers li {
+  margin-bottom: 0.5em;
+}
+#legend ul.addresses {
+  margin-bottom: 1.5em;
+}
+
+.code-branch:before {
+  font-style: normal;
+  font-family: FontAwesome;
+  content: "\f126";
+}
+
+  .legend-min-address {
+    padding-left: 15px;
+    opacity: 0;
+    z-index: 0;
+  }
+
+  div#debugging {
+    width: 20em;
+  }
+
+  .addresses .btn, #switches .btn {
+    padding: 0px 4px !important;
+    background-image: none;
+    box-shadow: 0 0 0;
+    border-color: black;
+  }
+
+  #popover-div {
+    position: absolute;
+    z-index: 200;
+    border-radius: 4px;
+    background-color: black;
+    color: white;
+    opacity: .95;
+    padding: 12px;
+    font-size: 14px;
+    display: none;
+  }
+
+  #chord text {
+    pointer-events: all;
+  }
+
+  #noTraffic {
+    position:absolute;
+  }
+  </style>
+  
+  <div ng-controller="QDR.ChordController">
+    <div id="popover-div" ng-bind-html="trustedpopoverContent"></div>
+    <div id="noTraffic"></div>
+    <div id="chord"></div>
+    <div id="legend">
+      <div ng-hide="arcColorsEmpty()">
+        <h4>Routers</h4>
+        <ul class="routers">
+          <li ng-repeat="(router, color) in arcColors" class="legend-line" ng-mouseover="enterRouter(router)" ng-mouseleave="leaveLegend()">
+            <span class='legend-color' ng-style="{'background-color': color}"></span>
+            <span class='legend-router' title="{{router}}">{{router | limitTo:15}}{{router.length>15 ? '…' : ''}}</span>
+          </li>
+        </ul>
+      </div>
+      <h4>Addresses</h4>
+      <ul class="addresses">
+        <li ng-repeat="(address, color) in addresses" class="legend-line">
+          <checkbox style="background-color: {{chordColors[address]}};"
+            title="{{address}}" ng-change="addressFilterChanged()"
+            ng-model="addresses[address]"></checkbox>
+          <span ng-mouseenter="enterLegend(address)" ng-mouseleave="leaveLegend()" ng-click="addressClick(address)" title="{{address}}">{{address | limitTo : 15}}{{address.length>15 ? '…' : ''}}</span>
+        </li>
+      </ul>
+    </div>
+    <div id="switches">
+      <ul>
+        <li>
+            <checkbox title="Select to show message rates" ng-model="legendOptions.isRate"></checkbox>
+            <span title="Select to show message rates">Show rates</span>
+        </li>
+        <li>
+            <checkbox title="Select to break out traffic by address" ng-model="legendOptions.byAddress"></checkbox>
+            <span title="Select to break out traffic by address">Show by address</span>
+        </li>
+      </ul>
+    </div>
+  </div>

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/55e08784/console/stand-alone/plugin/js/chord/data.js
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/js/chord/data.js b/console/stand-alone/plugin/js/chord/data.js
new file mode 100644
index 0000000..7f28a9c
--- /dev/null
+++ b/console/stand-alone/plugin/js/chord/data.js
@@ -0,0 +1,228 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+*/
+
+'use strict';
+/* global angular Promise MIN_CHORD_THRESHOLD */
+
+const SAMPLES = 3;  // number of snapshots to use for rate calculations
+
+function ChordData (QDRService, isRate, converter) {
+  this.QDRService = QDRService;
+  this.last_matrix = undefined;
+  this.last_values = {values: undefined, timestamp: undefined};
+  this.rateValues = undefined;
+  this.snapshots = [];  // last N values used for calculating rate
+  this.isRate = isRate;
+  // fn to convert raw data to matrix
+  this.converter = converter;
+  // object that determines which addresses are excluded
+  this.filter = [];
+}
+ChordData.prototype.setRate = function (isRate) {
+  this.isRate = isRate;
+};
+ChordData.prototype.setConverter = function (converter) {
+  this.converter = converter;
+};
+ChordData.prototype.setFilter = function (filter) {
+  this.filter = filter;
+};
+ChordData.prototype.getAddresses = function () {
+  let addresses = {};
+  let outer = this.snapshots;
+  if (outer.length === 0)
+    outer = outer = [this.last_values];
+  outer.forEach( function (snap) {
+    snap.values.forEach( function (lv) {
+      if (!(lv.address in addresses)) {
+        addresses[lv.address] = this.filter.indexOf(lv.address) < 0;
+      }
+    }, this);
+  }, this);
+  return addresses;
+};
+ChordData.prototype.getRouters = function () {
+  let routers = {};
+  let outer = this.snapshots;
+  if (outer.length === 0)
+    outer = [this.last_values];
+  outer.forEach( function (snap) {
+    snap.values.forEach( function (lv) {
+      routers[lv.egress] = true;
+      routers[lv.ingress] = true;
+    });
+  });
+  return Object.keys(routers).sort();
+};
+
+ChordData.prototype.applyFilter = function (filter) {
+  if (filter)
+    this.setFilter(filter);
+
+  return new Promise( (function (resolve) {
+    resolve(convert(this, this.last_values));
+  }));
+};
+
+// construct a square matrix of the number of messages each router has egressed from each router
+ChordData.prototype.getMatrix = function () {
+  let self = this;
+  return new Promise( (function (resolve, reject) {
+    // get the router.node and router.link info
+    self.QDRService.management.topology.fetchAllEntities([
+      {entity: 'router.node', attrs: ['id', 'index']},
+      {entity: 'router.link', attrs: ['linkType', 'linkDir', 'owningAddr', 'ingressHistogram']}], 
+    function(results) {
+      if (!results) {
+        reject(Error('unable to fetch entities'));
+        return;
+      }
+      // the raw data received from the rouers
+      let values = [];
+
+      // for each router in the network
+      for (let nodeId in results) {
+        // get a map of router ids to index into ingressHistogram for the links for this router.
+        // each routers has a different order for the routers
+        let ingressRouters = [];
+        let routerNode = results[nodeId]['router.node'];
+        let idIndex = routerNode.attributeNames.indexOf('id');
+
+        // ingressRouters is an array of router names in the same order that the ingressHistogram values will be in
+        for (let i=0; i<routerNode.results.length; i++) {
+          ingressRouters.push(routerNode.results[i][idIndex]);
+        }
+
+        // the name of the router we are working on
+        let egressRouter = self.QDRService.management.topology.nameFromId(nodeId);
+
+        // loop through the router links for this router looking for out/endpoint/non-console links
+        let routerLinks = results[nodeId]['router.link'];
+        for (let i=0; i<routerLinks.results.length; i++) {
+          let link = self.QDRService.utilities.flatten(routerLinks.attributeNames, routerLinks.results[i]);
+          // if the link is an outbound/enpoint/non console
+          if (link.linkType === 'endpoint' && link.linkDir === 'out' && !link.owningAddr.startsWith('Ltemp.')) {
+            // keep track of the raw egress values as well as their ingress and egress routers and the address
+            for (let j=0; j<ingressRouters.length; j++) {
+              let messages = link.ingressHistogram[j];
+              if (messages) {
+                values.push({ingress: ingressRouters[j], 
+                  egress:  egressRouter, 
+                  address: self.QDRService.utilities.addr_text(link.owningAddr), 
+                  messages: messages});
+              }
+            }
+          }
+        }
+      }
+      // values is an array of objects like [{ingress: 'xxx', egress: 'xxx', address: 'xxx', messages: ###}, ....]
+
+      // convert the raw values array into a matrix object
+      let matrix = convert(self, values);
+
+      // resolve the promise
+      resolve(matrix);
+    });
+  }));
+};
+ChordData.prototype.convertUsing = function (converter) {
+  let values = this.isRate ? this.rateValues : this.last_values.values;
+
+  // convert the values to a matrix using the requested converter and the current filter
+  return converter(values, this.filter);
+};
+
+// Private functions
+
+// compare the current values to the last_values and return the rate/second
+let calcRate = function (values, last_values, snapshots) {
+  let now = Date.now();
+  if (!last_values.values) {
+    last_values.values = values;
+    last_values.timestamp = now - 1000;
+  }
+
+  // ensure the snapshots are initialized
+  if (snapshots.length < SAMPLES) {
+    for (let i=0; i<SAMPLES; i++) {
+      if (snapshots.length < i+1) {
+        snapshots[i] = angular.copy(last_values);
+        snapshots[i].timestamp = now - (1000 * (SAMPLES-i));
+      }
+    }
+  }
+  // remove oldest sample
+  snapshots.shift();
+  // add the new values to the end.
+  snapshots.push(angular.copy(last_values));
+
+  let oldest = snapshots[0];
+  let rateValues = [];
+  let elapsed = (now - oldest.timestamp) / 1000;
+  values.forEach( function (value) {
+
+    let rate = 0;
+    let total = 0;
+    snapshots.forEach ( function (snap) {
+      let last_index = snap.values.findIndex( function (lv) {
+        return lv.ingress === value.ingress &&
+              lv.egress === value.egress &&
+              lv.address === value.address; 
+      });
+      if (last_index >= 0) {
+        total += snap.values[last_index].messages;
+      }
+    });
+    rate = (value.messages - (total / snapshots.length)) / elapsed;
+
+    rateValues.push({ingress: value.ingress, 
+      egress: value.egress, 
+      address: value.address,
+      messages: Math.max(rate, MIN_CHORD_THRESHOLD)
+    });
+  });
+  return rateValues;
+};
+
+let genKeys = function (values) {
+  values.forEach( function (value) {
+    value.key = value.egress + value.ingress + value.address;
+  });
+};
+let sortByKeys = function (values) {
+  return values.sort( function (a, b) {
+    return a.key > b.key ? 1 : a.key < b.key ? -1 : 0;
+  });
+};
+let convert = function (self, values) {
+  // sort the raw data by egress router name
+  genKeys(values);
+  sortByKeys(values);
+
+  self.last_values.values = angular.copy(values);
+  self.last_values.timestamp = Date.now();
+  if (self.isRate) {
+    self.rateValues = values = calcRate(values, self.last_values, self.snapshots);
+  }
+  // convert the raw data to a matrix
+  let matrix = self.converter(values, self.filter);
+  self.last_matrix = matrix;
+
+  return matrix;
+};

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/55e08784/console/stand-alone/plugin/js/chord/filters.js
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/js/chord/filters.js b/console/stand-alone/plugin/js/chord/filters.js
new file mode 100644
index 0000000..7bb68cc
--- /dev/null
+++ b/console/stand-alone/plugin/js/chord/filters.js
@@ -0,0 +1,107 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+*/
+'use strict';
+/* global valuesMatrix */
+
+// this filter will show an arc per router with the addresses aggregated
+var aggregateAddresses = function (values, filter) { // eslint-disable-line no-unused-vars
+  let m = new valuesMatrix(true);
+  values.forEach (function (value) {
+    if (filter.indexOf(value.address) < 0) {
+      let chordName = value.egress;
+      let egress = value.ingress;
+      let row = m.indexOf(chordName);
+      if (row < 0) {
+        row = m.addRow(chordName, value.ingress, value.egress, value.address);
+      }
+      let col = m.indexOf(egress);
+      if (col < 0) {
+        col = m.addRow(egress, value.ingress, value.egress, value.address);
+      }
+      m.addValue(row, col, value);
+    }
+  });
+  return m.sorted();
+};
+
+// this filter will show an arc per router-address
+var _separateAddresses = function (values, filter) { // eslint-disable-line no-unused-vars
+  let m = new valuesMatrix(false);
+  values = values.filter( function (v) { return filter.indexOf(v.address) < 0;});
+  if (values.length === 0)
+    return m;
+
+  let addresses = {}, routers = {};
+  // get the list of routers and addresses in the data
+  values.forEach( function (value) {
+    addresses[value.address] = true;
+    routers[value.ingress] = true;
+    routers[value.egress] = true;
+  });
+  let saddresses = Object.keys(addresses).sort();
+  let srouters = Object.keys(routers).sort();
+  let alen = saddresses.length;
+  // sanity check
+  if (alen === 0)
+    return m;
+
+  /* Convert the data to a matrix */
+
+  // initialize the matrix to have the correct ingress, egress, and address in each row and col
+  m.zeroInit(saddresses.length * srouters.length);
+  m.rows.forEach( function (row, r) {
+    let egress = srouters[Math.floor(r/alen)];
+    row.cols.forEach( function (col, c) {
+      let ingress = srouters[Math.floor(c/alen)];
+      let address = saddresses[c % alen];
+      m.setRowCol(r, c, ingress, egress, address, 0);
+    });
+  });
+  // set the values at each cell in the matrix
+  for (let i=0, alen=saddresses.length, vlen=values.length; i<vlen; i++) {
+    let value = values[i];
+    let egressIndex = srouters.indexOf(value.egress);
+    let ingressIndex = srouters.indexOf(value.ingress);
+    let addressIndex = saddresses.indexOf(value.address);
+    let row = egressIndex * alen + addressIndex;
+    let col = ingressIndex * alen + addressIndex;
+    m.setColMessages(row, col, value.messages);
+  }
+  return m;
+};
+
+let separateAddresses = function (values, filter) { // eslint-disable-line no-unused-vars
+  let m = new valuesMatrix(false);
+  values.forEach( function (value) {
+    if (filter.indexOf(value.address) < 0) {
+      let egressChordName = value.egress + value.ingress + value.address;
+      let r = m.indexOf(egressChordName);
+      if (r < 0) {
+        r = m.addRow(egressChordName, value.ingress, value.egress, value.address);
+      }
+      let ingressChordName = value.ingress + value.egress + value.address;
+      let c = m.indexOf(ingressChordName);
+      if (c < 0) {
+        c = m.addRow(ingressChordName, value.egress, value.ingress, value.address);
+      }
+      m.addValue(r, c, value);
+    }
+  });
+  return m.sorted();
+};

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/55e08784/console/stand-alone/plugin/js/chord/layout/README.md
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/js/chord/layout/README.md b/console/stand-alone/plugin/js/chord/layout/README.md
new file mode 100644
index 0000000..2a17d29
--- /dev/null
+++ b/console/stand-alone/plugin/js/chord/layout/README.md
@@ -0,0 +1,66 @@
+  This is a replacement for d3.layout.chord().
+  It implements a groubBy feature that allows arcs to be grouped.
+  It does not implement the sortBy features of d3.layout.chord.
+
+  API: 
+  qrdlayoutChord().padding(ARCPADDING).groupBy(groupByIndexes).matrix(matrix);
+  where groupByIndexes is an array of integers.
+
+  When grouping arcs together, you are taking multiple source arcs and combining them into a single target arc.
+  With grouping you can end up with multiple chords that start and stop on the same arc[s].
+
+  Each element in the groupByIndexes array corresponds to a row in the matrix, therefore the array
+  should be matrix.length long. The position in the groupByIndexes array specifies the 
+  source arc. The value at that position determines the target arc. If the groupByIndexes array has 2 unique
+  values (0 and 1) then there will be 2 groups returned.
+
+  For example: With a matrix of 
+     [[1,2], 
+      [3,4]]
+  that represents the trips between 2 neighbourhoods: A and B.
+  d3 would normally generate 2 arcs and 3 chords.
+  The 1st arc corresponds to A with data of [1.2], and the 2nd to B with data of [3,4].
+  Chord 1 would be from A to A with a value of 1.
+  Chord 2 would be between A and B with B having a value of 3 and A having a value of 2
+  Chord 3 would be from B to B with a value of 4.
+
+  If you had data that splits those same trips into by bike and on foot,
+  you could generate a more detailed matrix like:
+      [[0,0,0,1],
+       [0,1,1,0],
+       [0,2,3,0],
+       [1,0,0,1]
+      ]
+
+  This would generate 4 arcs and 5 chords.
+  The chords would be:
+  A foot - A foot value 1
+  A bike - B bike values 1 and 1
+  B foot - A foot values 1 and 2
+  B bike - B bike value 3
+  B foot - B foot value 1
+
+  But you don't want 4 arcs: A bike, A foot, B bike, and B foot. You want 2 arcs A and B with chords
+  between them that represent the bike and foot trips. 
+  Even though you could color the A bike and A foot arcs the same, there would still be a gap between them
+  and if you switched between the detailed matrix and the aggregate matrix, the arcs would move.
+  Also, with 4 arcs, the arc labels could get unruly.
+
+  One possible kludge would be to generate the detailed diagram with 0 padding between arcs and 
+  insert dummy rows and columns between the groups.
+  The values for the dummy entries would need to be calculated so that their arc size exactly corresponded 
+  to the normal padding.
+  The arcs and chords for the dummy data would have opacity 0 and not respond to mouse events. You'd also have to 
+  create and position the labels separatly from the arcs.
+
+  Or... you could use groupBy.
+  The detail matrix would stay the same. The output chords would be the same. The only change would be
+  that the arcs A bike and A foot would be combined, and the arcs B bike and B foot would be combined. 
+  In the above example you set the groupBy array to  [0,0,1,1].
+  This says the 1st two arcs get grouped together into a new arc 0, and the 2nd two arcs get grouped into
+  a new arc 1.
+
+  Since there can be chords that have the same source.index and source.subindex and the same target.index and 
+  target.subindex, two additional data values  are returned in the chord's source and target data structures: 
+  orgindex and orgsubindex. This will let you determine whether the chord is for 
+  bike trips or foot trips.

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/55e08784/console/stand-alone/plugin/js/chord/layout/layout.js
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/js/chord/layout/layout.js b/console/stand-alone/plugin/js/chord/layout/layout.js
new file mode 100644
index 0000000..e3d223f
--- /dev/null
+++ b/console/stand-alone/plugin/js/chord/layout/layout.js
@@ -0,0 +1,147 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+*/
+'use strict';
+/* global d3 */
+
+
+var qrdlayoutChord = function() { // eslint-disable-line no-unused-vars
+  var chord = {}, chords, groups, matrix, n, padding = 0, τ = Math.PI*2, groupBy;
+  function relayout() {
+    groupBy = groupBy || d3.range(n);
+    // number of unique values in the groupBy array. This will be the number
+    // of groups generated.
+    var groupLen = unique(groupBy);
+    var subgroups = {}, groupSums = fill(0, groupLen), k, x, x0, i, j, di, ldi;
+
+    chords = [];
+    groups = [];
+
+    // calculate the sum of the values for each group
+    k = 0, i = -1;
+    while (++i < n) {
+      x = 0, j = -1;
+      while (++j < n) {
+        x += matrix[i][j];
+      }
+      groupSums[groupBy[i]] += x;
+      k += x;
+    }
+    // the fraction of the circle for each incremental value
+    k = (τ - padding * groupLen) / k;
+    // for each row
+    x = 0, i = -1, ldi = groupBy[0];
+    while (++i < n) {
+      di = groupBy[i];
+      // insert padding after each group
+      if (di !== ldi) {
+        x += padding;
+        ldi = di;
+      }
+      // for each column
+      x0 = x, j = -1;
+      while (++j < n) {
+        var dj = groupBy[j], v = matrix[i][j], a0 = x, a1 = x += v * k;
+        // create a structure for each cell in the matrix. these are the potential chord ends
+        subgroups[i + '-' + j] = {
+          index: di,
+          subindex: dj,
+          orgindex: i,
+          orgsubindex: j,
+          startAngle: a0,
+          endAngle: a1,
+          value: v
+        };
+      }
+      if (!groups[di]) {
+        // create a new group (arc)
+        groups[di] = {
+          index: di,
+          startAngle: x0,
+          endAngle: x,
+          value: groupSums[di]
+        };
+      } else {
+        // bump up the ending angle of the combined arc
+        groups[di].endAngle = x;
+      }
+    }
+
+    // put the chord ends together into a chords.
+    i = -1;
+    while (++i < n) {
+      j = i - 1;
+      while (++j < n) {
+        var source = subgroups[i + '-' + j], target = subgroups[j + '-' + i];
+        // Only make a chord if there is a value at one of the two ends
+        if (source.value || target.value) {
+          chords.push(source.value < target.value ? {
+            source: target,
+            target: source
+          } : {
+            source: source,
+            target: target
+          });
+        }
+      }
+    }
+  }
+  chord.matrix = function(x) {
+    if (!arguments.length) return matrix;
+    n = (matrix = x) && matrix.length;
+    chords = groups = null;
+    return chord;
+  };
+  chord.padding = function(x) {
+    if (!arguments.length) return padding;
+    padding = x;
+    chords = groups = null;
+    return chord;
+  };
+  chord.groupBy = function (x) {
+    if (!arguments.length) return groupBy;
+    groupBy = x;
+    chords = groups = null;
+    return chord;
+  };
+  chord.chords = function() {
+    if (!chords) relayout();
+    return chords;
+  };
+  chord.groups = function() {
+    if (!groups) relayout();
+    return groups;
+  };
+  return chord;
+};
+
+let fill = function (value, length) {
+  var i=0, array = []; 
+  array.length = length; 
+  while(i < length) 
+    array[i++] = value;
+  return array;
+};
+
+let unique = function (arr) {
+  var counts = {};
+  for (var i = 0; i < arr.length; i++) {
+    counts[arr[i]] = 1 + (counts[arr[i]] || 0);
+  }
+  return Object.keys(counts).length;
+};

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/55e08784/console/stand-alone/plugin/js/chord/matrix.js
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/js/chord/matrix.js b/console/stand-alone/plugin/js/chord/matrix.js
new file mode 100644
index 0000000..d8b9456
--- /dev/null
+++ b/console/stand-alone/plugin/js/chord/matrix.js
@@ -0,0 +1,199 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+*/
+'use strict';
+/* global d3 */
+
+const MIN_CHORD_THRESHOLD = 0.01;
+
+// public Matrix object
+function valuesMatrix(aggregate) {
+  this.rows = [];
+  this.aggregate = aggregate;
+}
+// a matrix row
+let valuesMatrixRow = function(r, chordName, ingress, egress) {
+  this.chordName = chordName || '';
+  this.ingress = ingress || '';
+  this.egress = egress || '';
+  this.index = r;
+  this.cols = [];
+  for (let c=0; c<r; c++) {
+    this.addCol(0);
+  }
+};
+// a matrix column
+let valuesMatrixCol = function(messages, row, c, address) {
+  this.messages = messages;
+  this.address = address;
+  this.index = c;
+  this.row = row;
+};
+
+// initialize a matrix with empty data with size rows and columns
+valuesMatrix.prototype.zeroInit = function (size) {
+  for (let r=0; r<size; r++) {
+    this.addRow();
+  }
+};
+
+valuesMatrix.prototype.setRowCol = function (r, c, ingress, egress, address, value) {
+  this.rows[r].ingress = ingress;
+  this.rows[r].egress = egress;
+  this.rows[r].cols[c].messages = value;
+  this.rows[r].cols[c].address = address;
+};
+
+valuesMatrix.prototype.setColMessages = function (r, c, messages) {
+  this.rows[r].cols[c].messages = messages;
+};
+
+// return true if any of the matrix cells have messages
+valuesMatrix.prototype.hasValues = function () {
+  return this.rows.some(function (row) {
+    return row.cols.some(function (col) {
+      return col.messages > MIN_CHORD_THRESHOLD;
+    });
+  });
+};
+
+// extract a square matrix with just the values from the object matrix
+valuesMatrix.prototype.matrixMessages = function () {
+  let m = emptyMatrix(this.rows.length);
+  this.rows.forEach( function (row, r) {
+    row.cols.forEach( function (col, c) {
+      m[r][c] = col.messages;
+    });
+  });
+  return m;
+};
+
+valuesMatrix.prototype.getGroupBy = function () {
+  if (!this.aggregate && this.rows.length) {
+    let groups = [];
+    let lastName = this.rows[0].egress, groupIndex = 0;
+    this.rows.forEach( function (row) {
+      if (row.egress !== lastName) {
+        groupIndex++;
+        lastName = row.egress;
+      }
+      groups.push(groupIndex);
+    });
+    return groups;
+  }
+  else 
+    return d3.range(this.rows.length);
+};
+
+valuesMatrix.prototype.chordName = function (i, ingress) {
+  if (this.aggregate)
+    return this.rows[i].chordName;
+  return (ingress ? this.rows[i].ingress : this.rows[i].egress);
+};
+valuesMatrix.prototype.routerName = function (i) {
+  if (this.aggregate)
+    return this.rows[i].chordName;
+  return getAttribute(this, 'egress', i);
+};
+valuesMatrix.prototype.getEgress = function (i) {
+  return getAttribute(this, 'egress', i);
+};
+valuesMatrix.prototype.getIngress = function (i) {
+  return getAttribute(this, 'ingress', i);
+};
+valuesMatrix.prototype.getAddress = function (r, c) {
+  return this.rows[r].cols[c].address;
+};
+valuesMatrix.prototype.getAddresses = function (r) {
+  let addresses = {};
+  this.rows[r].cols.forEach( function (c) {
+    if (c.address && c.messages)
+      addresses[c.address] = true;
+  });
+  return Object.keys(addresses);
+};
+let getAttribute = function (self, attr, i) {
+  if (self.aggregate)
+    return self.rows[i][attr];
+  return self.rows[self.getGroupBy().indexOf(i)][attr];
+};
+valuesMatrix.prototype.addRow = function (chordName, ingress, egress, address) {
+  let rowIndex = this.rows.length;
+  let newRow = new valuesMatrixRow(rowIndex, chordName, ingress, egress);
+  this.rows.push(newRow);
+  // add new column to all rows
+  for (let r=0; r<=rowIndex; r++) {
+    this.rows[r].addCol(0, address);
+  }
+  return rowIndex;
+};
+valuesMatrix.prototype.indexOf = function (chordName) {
+  return this.rows.findIndex( function (row) {
+    return row.chordName === chordName;
+  });
+};
+valuesMatrix.prototype.addValue = function (r, c, value) {
+  this.rows[r].cols[c].addMessages(value.messages);
+  this.rows[r].cols[c].setAddress(value.address);
+};
+valuesMatrixRow.prototype.addCol = function (messages, address) {
+  this.cols.push(new valuesMatrixCol(messages, this, this.cols.length, address));
+};
+valuesMatrixCol.prototype.addMessages = function (messages) {
+  if (!(this.messages === MIN_CHORD_THRESHOLD && messages === MIN_CHORD_THRESHOLD))
+    this.messages += messages;
+};
+valuesMatrixCol.prototype.setAddress = function (address) {
+  this.address = address;
+};
+valuesMatrix.prototype.getChordList = function () {
+  return this.rows.map( function (row) {
+    return row.chordName;
+  });
+};
+valuesMatrix.prototype.sorted = function () {
+  let newChordList = this.getChordList();
+  newChordList.sort();
+  let m = new valuesMatrix(this.aggregate);
+  m.zeroInit(this.rows.length);
+  this.rows.forEach( function (row) {
+    let chordName = row.chordName;
+    row.cols.forEach( function (col, c) {
+      let newRow = newChordList.indexOf(chordName);
+      let newCol = newChordList.indexOf(this.rows[c].chordName);
+      m.rows[newRow].chordName = chordName;
+      m.rows[newRow].ingress = row.ingress;
+      m.rows[newRow].egress = row.egress;
+      m.rows[newRow].cols[newCol].messages = col.messages;
+      m.rows[newRow].cols[newCol].address = col.address;
+    }.bind(this));
+  }.bind(this));
+  return m;
+};
+
+// private helper function
+let emptyMatrix = function (size) {
+  let matrix = [];
+  for(let i=0; i<size; i++) {
+    matrix[i] = [];
+    for(let j=0; j<size; j++) {
+      matrix[i][j] = 0;
+    }
+  }
+  return matrix;
+};

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/55e08784/console/stand-alone/plugin/js/chord/qdrChord.js
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/js/chord/qdrChord.js b/console/stand-alone/plugin/js/chord/qdrChord.js
new file mode 100644
index 0000000..c5f7417
--- /dev/null
+++ b/console/stand-alone/plugin/js/chord/qdrChord.js
@@ -0,0 +1,823 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+*/
+'use strict';
+/* global angular d3 separateAddresses aggregateAddresses ChordData qdrRibbon qrdlayoutChord */
+
+var QDR = (function (QDR) {
+  QDR.module.controller('QDR.ChordController', ['$scope', 'QDRService', '$location', '$timeout', '$sce', function($scope, QDRService, $location, $timeout, $sce) {
+
+    // if we get here and there is no connection, redirect to the connect page and then 
+    // return here once we are connected
+    if (!QDRService.management.connection.is_connected()) {
+      QDR.redirectWhenConnected($location, 'chord');
+      return;
+    }
+
+    const CHORDOPTIONSKEY = 'chordOptions';
+    const CHORDFILTERKEY =  'chordFilter';
+    const DOUGHNUT =        '#chord svg .empty';
+    const ERROR_RENDERING = 'Error while rendering ';
+    const ARCPADDING = .06;
+
+    // flag to show/hide the router section of the legend
+    $scope.noValues = true;
+    // state of the option checkboxes
+    $scope.legendOptions = angular.fromJson(localStorage[CHORDOPTIONSKEY]) || {isRate: false, byAddress: false};
+    // remember which addresses were last selected and restore them when page loads
+    let excludedAddresses = angular.fromJson(localStorage[CHORDFILTERKEY]) || [];
+    // colors for the legend and the diagram
+    $scope.chordColors = {};
+    $scope.arcColors = {};
+
+    // get notified when the byAddress checkbox is toggled
+    let switchedByAddress = false;
+    $scope.$watch('legendOptions.byAddress', function (newValue, oldValue) {
+      if (newValue !== oldValue) {
+        d3.select('#legend')
+          .classed('byAddress', newValue);
+        chordData.setConverter($scope.legendOptions.byAddress ? separateAddresses: aggregateAddresses);
+        switchedByAddress = true;
+        updateNow();
+        localStorage[CHORDOPTIONSKEY] = JSON.stringify($scope.legendOptions);
+      }
+    });
+    // get notified when the 'by rate' checkbox is toggled
+    $scope.$watch('legendOptions.isRate', function (n, o) {
+      if (n !== o) {
+        chordData.setRate($scope.legendOptions.isRate);
+
+        let doughnut = d3.select(DOUGHNUT);
+        if (!doughnut.empty()) {
+          fadeDoughnut();
+        }
+        updateNow();
+
+        localStorage[CHORDOPTIONSKEY] = JSON.stringify($scope.legendOptions);
+      }
+    });
+    $scope.arcColorsEmpty = function () {
+      return Object.keys($scope.arcColors).length === 0;
+    };
+
+    // event notification that an address checkbox has changed
+    $scope.addressFilterChanged = function () {
+      fadeDoughnut();
+
+      excludedAddresses = [];
+      for (let address in $scope.addresses) {
+        if (!$scope.addresses[address])
+          excludedAddresses.push(address);
+      }
+      localStorage[CHORDFILTERKEY] = JSON.stringify(excludedAddresses);
+      if (chordData) 
+        chordData.setFilter(excludedAddresses);
+      updateNow();
+    };
+
+    // called by angular when mouse enters one of the address legends
+    $scope.enterLegend = function (addr) {
+      if (!$scope.legendOptions.byAddress)
+        return;
+      // fade all chords that don't have this address 
+      let indexes = [];
+      chordData.last_matrix.rows.forEach( function (row, r) {
+        let addresses = chordData.last_matrix.getAddresses(r);
+        if (addresses.indexOf(addr) >= 0)
+          indexes.push(r);
+      });
+      d3.selectAll('path.chord').classed('fade', function(p) {
+        return indexes.indexOf(p.source.orgindex) < 0 && indexes.indexOf(p.target.orgindex) < 0;
+      });
+    };
+
+    // called by angular when mouse enters one of the router legends
+    $scope.enterRouter = function (router) {
+      let indexes = [];
+      // fade all chords that are not associated with this router
+      let agg = chordData.last_matrix.aggregate;
+      chordData.last_matrix.rows.forEach( function (row, r) {
+        if (agg) {
+          if (row.chordName === router)
+            indexes.push(r);
+        } else {
+          if (row.ingress === router || row.egress === router)
+            indexes.push(r);
+        }
+      });
+      d3.selectAll('path.chord').classed('fade', function(p) {
+        return indexes.indexOf(p.source.orgindex) < 0 && indexes.indexOf(p.target.orgindex) < 0;
+      });
+    };
+    $scope.leaveLegend = function () {
+      showAllChords();
+    };
+    // clicked on the address name. toggle the address checkbox
+    $scope.addressClick = function (address) {
+      $scope.addresses[address] = !$scope.addresses[address];
+      $scope.addressFilterChanged();
+    };
+
+    // fade out the empty circle that is shown when there is no traffic
+    let fadeDoughnut = function () {
+      d3.select(DOUGHNUT)
+        .transition()
+        .duration(200)
+        .attr('opacity', 0)
+        .remove();
+    };
+
+    // create an object that will be used to fetch the data
+    let chordData = new ChordData(QDRService, 
+      $scope.legendOptions.isRate, 
+      $scope.legendOptions.byAddress ? separateAddresses: aggregateAddresses);
+    chordData.setFilter(excludedAddresses);
+
+    // get the data now in response to a user event (as opposed to periodically)
+    let updateNow = function () {
+      clearInterval(interval);
+      chordData.getMatrix().then(render, function (e) { console.log(ERROR_RENDERING + e);});
+      interval = setInterval(doUpdate, transitionDuration);
+    };
+
+    // size the diagram based on the browser window size
+    let getRadius = function () {
+      let w = window,
+        d = document,
+        e = d.documentElement,
+        b = d.getElementsByTagName('body')[0],
+        x = w.innerWidth || e.clientWidth || b.clientWidth,
+        y = w.innerHeight|| e.clientHeight|| b.clientHeight;
+      return Math.max(Math.floor((Math.min(x, y) * 0.9) / 2), 300);
+    };
+
+    // diagram sizes that change when browser is resized
+    let outerRadius, innerRadius, textRadius;
+    let setSizes = function () {
+      // size of circle + text
+      outerRadius = getRadius();
+      // size of chords
+      innerRadius = outerRadius - 130;
+      // arc ring around chords
+      textRadius = Math.min(innerRadius * 1.1, innerRadius + 15);
+    };
+    setSizes();
+
+    // TODO: handle window resizes
+    //let updateWindow  = function () {
+    //setSizes();
+    //startOver();
+    //};
+    //d3.select(window).on('resize.updatesvg', updateWindow);
+    let offBy = 6;
+    let windowResized = function () {
+      let legendPos = $('#legend').position();
+      let switches = $('#switches');
+      let outerWidth = switches.outerWidth();
+      switches.css({left: (legendPos.left - outerWidth - offBy), opacity: 1});
+      offBy = 0;
+    };
+    window.addEventListener('resize', function () {
+      windowResized()
+      setTimeout(windowResized, 1);
+    });
+    $().ready(windowResized);
+
+    // used for animation duration and the data refresh interval 
+    let transitionDuration = 1000;
+    // format with commas
+    let formatNumber = d3.format(',.1f');
+
+    // colors
+    let colorGen = d3.scale.category20();
+    // The colorGen funtion is not random access. 
+    // To get the correct color[19] you first have to get all previous colors
+    // I suspect some caching is going on in d3
+    for (let i=0; i<20; i++) {
+      colorGen(i);
+    }
+    // arc colors are taken from every other color starting at 0
+    let getArcColor = function (n) {
+      if (!(n in $scope.arcColors)) {
+        let ci = Object.keys($scope.arcColors).length * 2;
+        $scope.arcColors[n] = colorGen(ci);
+      }
+      return $scope.arcColors[n];
+    };
+    // chord colors are taken from every other color starting at 19 and going backwards
+    let getChordColor = function (n) {
+      if (!(n in $scope.chordColors)) {
+        let ci = 19 - Object.keys($scope.chordColors).length * 2;
+        let c = colorGen(ci);
+        $scope.chordColors[n] = c;
+      }
+      return $scope.chordColors[n];
+    };
+    // return the color associated with a router
+    let fillArc = function (matrixValues, row) {
+      let router = matrixValues.routerName(row);
+      return getArcColor(router);
+    };
+    // return the color associated with a chord.
+    // if viewing by address, the color will be the address color.
+    // if viewing aggregate, the color will be the router color of the largest chord ending
+    let fillChord = function (matrixValues, d) {
+      // aggregate
+      if (matrixValues.aggregate) {
+        return fillArc(matrixValues, d.source.index);
+      }
+      // by address
+      let addr = matrixValues.getAddress(d.source.orgindex, d.source.orgsubindex);
+      return getChordColor(addr);
+    };
+
+    // keep track of previous chords so we can animate to the new values
+    let last_chord, last_labels;
+
+    // global pointer to the diagram
+    let svg;
+
+    // called once when the page loads and again
+    // whenever the number of routers that have egressed messages changes
+    let initSvg = function () {
+      d3.select('#chord svg').remove();
+
+      svg = d3.select('#chord').append('svg')
+        .attr('width', outerRadius * 2)
+        .attr('height', outerRadius * 2)
+        .append('g')
+        .attr('id', 'circle')
+        .attr('transform', 'translate(' + outerRadius + ',' + outerRadius + ')');
+
+      // mouseover target for when the mouse leaves the diagram
+      svg.append('circle')
+        .attr('r', innerRadius * 2)
+        .on('mouseover', showAllChords);
+
+      // background circle. will only get a mouseover event if the mouse is between chords
+      svg.append('circle')
+        .attr('r', innerRadius)
+        .on('mouseover', function() { d3.event.stopPropagation(); });
+
+      svg = svg.append('g')
+        .attr('class', 'chart-container');
+    };
+    initSvg();
+
+    let emptyCircle = function () {
+      $scope.noValues = false;
+      d3.select(DOUGHNUT).remove();
+
+      let arc = d3.svg.arc()
+        .innerRadius(innerRadius)
+        .outerRadius(textRadius)
+        .startAngle(0)
+        .endAngle(Math.PI * 2);
+
+      d3.select('#circle').append('path')
+        .attr('class', 'empty')
+        .attr('d', arc);
+    };
+
+    let genArcColors = function () {
+      //$scope.arcColors = {};
+      let routers = chordData.getRouters();
+      routers.forEach( function (router) {
+        getArcColor(router);
+      });
+    };
+    let genChordColors = function () {
+      $scope.chordColors = {};
+      if ($scope.legendOptions.byAddress) {
+        Object.keys($scope.addresses).forEach( function (address) {
+          getChordColor(address);
+        });
+      }
+    };
+    let chordKey = function (d, matrix) {
+      // sort so that if the soure and target are flipped, the chord doesn't
+      // get destroyed and recreated
+      return getRouterNames(d, matrix).sort().join('-');
+    };
+    let popoverChord = null;
+    let popoverArc = null;
+
+    let getRouterNames = function (d, matrix) {
+      let egress, ingress, address = '';
+      // for arcs d will have an index, for chords d will have a source.index and target.index
+      let eindex = angular.isDefined(d.index) ? d.index : d.source.index;
+      let iindex = angular.isDefined(d.index) ? d.index : d.source.subindex;
+      if (matrix.aggregate) {
+        egress = matrix.rows[eindex].chordName;
+        ingress = matrix.rows[iindex].chordName;
+      } else {
+        egress = matrix.routerName(eindex);
+        ingress = matrix.routerName(iindex);
+        // if a chord
+        if (d.source) {
+          address = matrix.getAddress(d.source.orgindex, d.source.orgsubindex);
+        }
+      }
+      return [ingress, egress, address];
+    };
+    // popup title when mouse is over a chord
+    // shows the address, from and to routers, and the values
+    let chordTitle = function (d, matrix) {
+      let rinfo = getRouterNames(d, matrix);
+      let from = rinfo[0], to = rinfo[1], address = rinfo[2];
+      if (!matrix.aggregate) {
+        address += '<br/>';
+      }
+      let title = address + from
+      + ' → ' + to
+      + ': ' + formatNumber(d.source.value);
+      if (d.target.value > 0 && to !== from) {
+        title += ('<br/>' + to
+        + ' → ' + from
+        + ': ' + formatNumber(d.target.value));
+      }
+      return title;
+    };
+    let arcTitle = function (d, matrix) {
+      let egress, value = 0;
+      if (matrix.aggregate) {
+        egress = matrix.rows[d.index].chordName;
+        value = d.value;
+      }
+      else {
+        egress = matrix.routerName(d.index);
+        value = d.value;
+      }
+      return egress + ': ' + formatNumber(value);
+    };
+
+    let decorateChordData = function (rechord, matrix) {
+      let data = rechord.chords();
+      data.forEach( function (d, i) {
+        d.key = chordKey(d, matrix, false);
+        d.orgIndex = i;
+        d.color = fillChord(matrix, d);
+      });
+      return data;
+    };
+
+    let decorateArcData = function (fn, matrix) {
+      let fixedGroups = fn();
+      fixedGroups.forEach( function (fg) {
+        fg.orgIndex = fg.index;
+        fg.angle = (fg.endAngle + fg.startAngle)/2;
+        fg.key = matrix.routerName(fg.index);
+        fg.components = [fg.index];
+        fg.router = matrix.aggregate ? fg.key : matrix.getEgress(fg.index);
+        fg.color = getArcColor(fg.router);
+      });
+      return fixedGroups;
+    };
+
+    let theyveBeenWarned = false;
+    // create and/or update the chord diagram
+    function render(matrix) {
+      $scope.addresses = chordData.getAddresses();
+      // populate the arcColors object with a color for each router
+      genArcColors();
+      genChordColors();
+
+      // if all the addresses are excluded, update the message
+      let addressLen = Object.keys($scope.addresses).length;
+      $scope.allAddressesFiltered = false;
+      if (addressLen > 0 && excludedAddresses.length === addressLen) {
+        $scope.allAddressesFiltered = true;
+      }
+
+      $scope.noValues = false;
+      let matrixMessages, duration = transitionDuration;
+
+      // if there is no data, show an empty circle and a message
+      if (!matrix.hasValues()) {
+        $timeout( function () {
+          $scope.noValues = $scope.arcColors.length === 0;
+          if (!theyveBeenWarned) {
+            theyveBeenWarned = true;
+            let msg = 'There is no message traffic';
+            if (addressLen !== 0)
+              msg += ' for the selected addresses';
+            $.notify($('#noTraffic'), msg, {clickToHide: false, autoHide: false, arrowShow: false, className: 'Warning'});
+          }
+        });
+        emptyCircle();
+        matrixMessages = [];
+      } else {
+        matrixMessages = matrix.matrixMessages();
+        $('.notifyjs-wrapper').hide();
+        theyveBeenWarned = false;
+        fadeDoughnut();
+      }
+
+      // create a new chord layout so we can animate between the last one and this one
+      let groupBy = matrix.getGroupBy();
+      let rechord = qrdlayoutChord().padding(ARCPADDING).groupBy(groupBy).matrix(matrixMessages);
+
+      // The chord layout has a function named .groups() that returns the
+      // data for the arcs. We decorate this data with a unique key.
+      rechord.arcData = decorateArcData(rechord.groups, matrix);
+
+      // join the decorated data with a d3 selection
+      let arcsGroup = svg.selectAll('g.arc')
+        .data(rechord.arcData, function (d) {return d.key;});
+
+      // get a d3 selection of all the new arcs that have been added
+      let newArcs = arcsGroup.enter().append('svg:g')
+        .attr('class', 'arc');
+
+      // each new arc is an svg:path that has a fixed color
+      newArcs.append('svg:path')
+        .style('fill', function(d) { return d.color; })
+        .style('stroke', function(d) { return d.color; });
+
+      newArcs.append('svg:text')
+        .attr('dy', '.35em')
+        .text(function (d) {
+          return d.router;
+        });
+
+      // attach event listeners to all arcs (new or old)
+      arcsGroup
+        .on('mouseover', mouseoverArc)
+        .on('mousemove', function (d) {
+          popoverArc = d;
+          let top = $('#chord').offset().top - 5;
+          $timeout(function () {
+            $scope.trustedpopoverContent = $sce.trustAsHtml(arcTitle(d, matrix));
+          });
+          d3.select('#popover-div')
+            .style('display', 'block')
+            .style('left', (d3.event.pageX+5)+'px')
+            .style('top', (d3.event.pageY-top)+'px');
+        })
+        .on('mouseout', function () {
+          popoverArc = null;
+          d3.select('#popover-div')
+            .style('display', 'none');
+        });
+
+      // animate the arcs path to it's new location
+      arcsGroup.select('path')
+        .transition()
+        .duration(duration)
+        //.ease('linear')
+        .attrTween('d', arcTween(last_chord));
+      arcsGroup.select('text')
+        .attr('text-anchor', function (d) {
+          return d.angle > Math.PI ? 'end' : 'begin';
+        })
+        .transition()
+        .duration(duration)
+        .attrTween('transform', tickTween(last_labels));
+
+      // check if the mouse is hovering over an arc. if so, update the tooltip
+      arcsGroup
+        .each(function(d) {
+          if (popoverArc && popoverArc.index === d.index) {
+            $scope.trustedpopoverContent = $sce.trustAsHtml(arcTitle(d, matrix));
+          }
+        });
+
+      // animate the removal of any arcs that went away
+      let exitingArcs = arcsGroup.exit();
+
+      exitingArcs.selectAll('text')
+        .transition()
+        .duration(duration/2)
+        .attrTween('opacity', function () {return function (t) {return 1 - t;};});
+
+      exitingArcs.selectAll('path')
+        .transition()
+        .duration(duration/2)
+        .attrTween('d', arcTweenExit)
+        .each('end', function (d) {d3.select(this).node().parentNode.remove();});
+
+      // decorate the chord layout's .chord() data with key, color, and orgIndex
+      rechord.chordData = decorateChordData(rechord, matrix);
+      let chordPaths = svg.selectAll('path.chord')
+        .data(rechord.chordData, function (d) { return d.key;});
+
+      // new chords are paths
+      chordPaths.enter().append('path')
+        .attr('class', 'chord');
+
+      if (!switchedByAddress) {
+        // do multiple concurrent tweens on the chords
+        chordPaths
+          .call(tweenChordEnds, duration, last_chord)
+          .call(tweenChordColor, duration, last_chord, 'stroke')
+          .call(tweenChordColor, duration, last_chord, 'fill');
+      } else {
+        // switchByAddress is only true when we have new chords
+        chordPaths
+          .attr('d', function (d) {return chordReference(d);})
+          .attr('stroke', function (d) {return d3.rgb(d.color).darker(1);})
+          .attr('fill', function (d) {return d.color;})
+          .attr('opacity', 1e-6)
+          .transition()
+          .duration(duration/2)
+          .attr('opacity', .67);
+      }
+  
+      // if the mouse is hovering over a chord, update it's tooltip
+      chordPaths
+        .each(function(d) {
+          if (popoverChord && 
+            popoverChord.source.orgindex === d.source.orgindex && 
+            popoverChord.source.orgsubindex === d.source.orgsubindex) {
+            $scope.trustedpopoverContent = $sce.trustAsHtml(chordTitle(d, matrix));
+          }
+        });
+
+      // attach mouse event handlers to the chords
+      chordPaths
+        .on('mouseover', mouseoverChord)
+        .on('mousemove', function (d) {
+          popoverChord = d;
+          let top = $('#chord').offset().top - 5;
+          $timeout(function () {
+            $scope.trustedpopoverContent = $sce.trustAsHtml(chordTitle(d, matrix));
+          });
+          d3.select('#popover-div')
+            .style('display', 'block')
+            .style('left', (d3.event.pageX+5)+'px')
+            .style('top', (d3.event.pageY-top)+'px');
+        })
+        .on('mouseout', function () {
+          popoverChord = null;
+          d3.select('#popover-div')
+            .style('display', 'none');
+        });
+
+      let exitingChords = chordPaths.exit()
+        .attr('class', 'exiting-chord');
+
+      if (!switchedByAddress) {
+        // shrink chords to their center point upon removal
+        exitingChords
+          .transition()
+          .duration(duration/2)
+          .attrTween('d', chordTweenExit)
+          .remove();
+      } else {
+        // just fade them out if we are switching between byAddress and aggregate
+        exitingChords
+          .transition()
+          .duration(duration/2)
+          .ease('linear')
+          .attr('opacity', 1e-6)
+          .remove();
+      }
+
+      // keep track of this layout so we can animate from this layout to the next layout
+      last_chord = rechord;
+      last_labels = last_chord.arcData;
+      switchedByAddress = false;
+
+      // update the UI for any $scope variables that changed
+      if(!$scope.$$phase) $scope.$apply();
+    }
+
+    // used to transition chords along a circular path instead of linear.
+    // qdrRibbon is a replacement for d3.svg.chord() that avoids the twists
+    let chordReference = qdrRibbon().radius(innerRadius);
+
+    // used to transition arcs along a curcular path instead of linear
+    let arcReference = d3.svg.arc()
+      .startAngle(function(d) { return d.startAngle; })
+      .endAngle(function(d) { return d.endAngle; })
+      .innerRadius(innerRadius)
+      .outerRadius(textRadius);
+
+    // animate the disappearance of an arc by shrinking it to its center point
+    function arcTweenExit(d) {
+      let angle = (d.startAngle+d.endAngle)/2;
+      let to = {startAngle: angle, endAngle: angle, value: 0};
+      let from = {startAngle: d.startAngle, endAngle: d.endAngle, value: d.value};
+      let tween = d3.interpolate(from, to);
+      return function (t) {
+        return arcReference( tween(t) );
+      };
+    }
+    // animate the exit of a chord by shrinking it to the center points of its arcs
+    function chordTweenExit(d) {
+      let angle = function (d) {
+        return (d.startAngle + d.endAngle) / 2;
+      };
+      let from = {source: {startAngle: d.source.startAngle, endAngle: d.source.endAngle}, 
+        target: {startAngle: d.target.startAngle, endAngle: d.target.endAngle}};
+      let to = {source: {startAngle: angle(d.source), endAngle: angle(d.source)},
+        target: {startAngle: angle(d.target), endAngle: angle(d.target)}};
+      let tween = d3.interpolate(from, to);
+
+      return function (t) {
+        return chordReference( tween(t) );
+      };
+    }
+
+    // Animate an arc from its old location to its new.
+    // If the arc is new, grow the arc from its startAngle to its full size
+    function arcTween(oldLayout) {
+      var oldGroups = {};
+      if (oldLayout) {
+        oldLayout.arcData.forEach( function(groupData) {
+          oldGroups[ groupData.index ] = groupData;
+        });
+      }
+      return function (d) {
+        var tween;
+        var old = oldGroups[d.index];
+        if (old) { //there's a matching old group
+          tween = d3.interpolate(old, d);
+        }
+        else {
+          //create a zero-width arc object
+          let mid = (d.startAngle + d.endAngle) / 2;
+          var emptyArc = {startAngle: mid, endAngle: mid};
+          tween = d3.interpolate(emptyArc, d);
+        }
+            
+        return function (t) {
+          return arcReference( tween(t) );
+        };
+      };
+    }
+
+    // animate all the chords to their new positions
+    function tweenChordEnds(chords, duration, last_layout) {
+      let oldChords = {};
+      if (last_layout) {
+        last_layout.chordData.forEach( function(d) {
+          oldChords[ d.key ] = d;
+        });
+      }
+      chords.each(function (d) {
+        let chord = d3.select(this);
+        // This version of d3 doesn't support multiple concurrent transitions on the same selection.
+        // Since we want to animate the chord's path as well as its color, we create a dummy selection
+        // and use that to directly transition each chord
+        d3.select({})
+          .transition()
+          .duration(duration)
+          .tween('attr:d', function () {
+            let old = oldChords[ d.key ], interpolate;
+            if (old) {
+              // avoid swapping the end of cords where the source/target have been flipped
+              // Note: the chord's colors will be swapped in a different tween
+              if (old.source.index === d.target.index &&
+                  old.source.subindex === d.target.subindex) {
+                let s = old.source;
+                old.source = old.target;
+                old.target = s;
+              }
+            } else {
+              // there was no old chord so make a fake one
+              let midStart = (d.source.startAngle + d.source.endAngle) / 2;
+              let midEnd = (d.target.startAngle + d.target.endAngle) / 2;
+              old = {
+                source: { startAngle: midStart,
+                  endAngle: midStart},
+                target: { startAngle: midEnd,
+                  endAngle: midEnd}
+              };
+            }
+            interpolate = d3.interpolate(old, d);
+            return function(t) {
+              chord.attr('d', chordReference(interpolate(t)));
+            };
+          });
+      });
+    }
+
+    // animate a chord to its new color
+    function tweenChordColor(chords, duration, last_layout, style) {
+      let oldChords = {};
+      if (last_layout) {
+        last_layout.chordData.forEach( function(d) {
+          oldChords[ d.key ] = d;
+        });
+      }
+      chords.each(function (d) {
+        let chord = d3.select(this);
+        d3.select({})
+          .transition()
+          .duration(duration)
+          .tween('style:'+style, function () {
+            let old = oldChords[ d.key ], interpolate;
+            let oldColor = '#CCCCCC', newColor = d.color;
+            if (old) {
+              oldColor = old.color;
+            }
+            if (style === 'stroke') {
+              oldColor = d3.rgb(oldColor).darker(1);
+              newColor = d3.rgb(newColor).darker(1);
+            }
+            interpolate = d3.interpolate(oldColor, newColor);
+            return function(t) {
+              chord.style(style, interpolate(t));
+            };
+          });
+      });
+    }
+
+    // animate the arc labels to their new locations
+    function tickTween(oldArcs) {
+      var oldTicks = {};
+      if (oldArcs) {
+        oldArcs.forEach( function(d) {
+          oldTicks[ d.key ] = d;
+        });
+      }
+      let angle = function (d) {
+        return (d.startAngle + d.endAngle) / 2;
+      };
+      return function (d) {
+        var tween;
+        var old = oldTicks[d.key];
+        let start = angle(d);
+        let startTranslate = textRadius - 40;
+        let orient = d.angle > Math.PI ? 'rotate(180)' : '';
+        if (old) { //there's a matching old group
+          start = angle(old);
+          startTranslate = textRadius;
+        }
+        tween = d3.interpolateNumber(start, angle(d));
+        let same = start === angle(d);
+        let tsame = startTranslate === textRadius;
+
+        let transTween = d3.interpolateNumber(startTranslate, textRadius + 10);
+
+        return function (t) {
+          let rot = same ? start : tween(t);
+          if (isNaN(rot))
+            rot = 0;
+          let tra = tsame ? (textRadius + 10) : transTween(t);
+          return 'rotate(' + (rot * 180 / Math.PI - 90) + ') '
+          + 'translate(' + tra + ',0)' + orient;
+        };
+      };
+    }
+
+    // fade all chords that don't belong to the given arc index
+    function mouseoverArc(d) {
+      d3.selectAll('path.chord').classed('fade', function(p) {
+        return d.index !== p.source.index && d.index !== p.target.index;
+      });
+    }
+
+    // fade all chords except the given one
+    function mouseoverChord(d) {
+      svg.selectAll('path.chord').classed('fade', function(p) {
+        return !(p.source.orgindex === d.source.orgindex && p.target.orgindex === d.target.orgindex);
+      });
+    }
+
+    function showAllChords() {
+      svg.selectAll('path.chord').classed('fade', false);
+    }
+
+    // when the page is exited
+    $scope.$on('$destroy', function() {
+      // stop updated the data
+      clearInterval(interval);
+      // clean up memory associated with the svg
+      d3.select('#chord').remove();
+      d3.select(window).on('resize.updatesvg', null);
+      window.removeEventListener('resize', windowResized);
+    });
+
+    // get the raw data and render the svg
+    chordData.getMatrix().then(render, function (e) {
+      console.log(ERROR_RENDERING + e);
+    });
+    // called periodically to refresh the data
+    function doUpdate() {
+      chordData.getMatrix().then(render, function (e) {
+        console.log(ERROR_RENDERING + e);
+      });
+    }
+    let interval = setInterval(doUpdate, transitionDuration);
+  
+  }]);
+  return QDR;
+
+} (QDR || {}));

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/55e08784/console/stand-alone/plugin/js/chord/ribbon/README.md
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/js/chord/ribbon/README.md b/console/stand-alone/plugin/js/chord/ribbon/README.md
new file mode 100644
index 0000000..18999c4
--- /dev/null
+++ b/console/stand-alone/plugin/js/chord/ribbon/README.md
@@ -0,0 +1,22 @@
+
+  This is a replacement for the d3.svg.chord() ribbon generator.
+  The native d3 implementation is efficient, but its chords can become 'twisted'
+  in certain curcumatances.
+
+  A chord has up to 4 components:
+  1. A beginning arc along the edge of a circle
+  2. A quadratic bezier curve across the circle to the start of another arc
+  3. A 2nd arc along the edge of the circle
+  4. A quadratic bezier curve to the start of the 1st arc
+
+  Components 2 and 3 are dropped if the chord has only one endpoint.
+
+  The problem arises when the arcs are very close to each other and one arc is significantly
+  larger than the other. The inner bezier curve connecting the arcs extends towards the center
+  of the circle. The outer bezier curve connecting the outer ends of the arc crosses the inner
+  bezier curve causing the chords to look twisted.
+
+  The solution implemented here is to adjust the inner bezier curve to not extend into the circle very far.
+  That is done by changing its control point. Instead of the control point being at the center
+  of the circle, it is moved towards the edge of the circle in the direction of the midpoint of 
+  the bezier curve's end points.

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/55e08784/console/stand-alone/plugin/js/chord/ribbon/ribbon.js
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/js/chord/ribbon/ribbon.js b/console/stand-alone/plugin/js/chord/ribbon/ribbon.js
new file mode 100644
index 0000000..604fa1e
--- /dev/null
+++ b/console/stand-alone/plugin/js/chord/ribbon/ribbon.js
@@ -0,0 +1,165 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+
+  http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+*/
+'use strict';
+/* global d3 */
+
+const halfPI = Math.PI / 2.0;
+const twoPI = Math.PI * 2.0;
+
+// These are scales to interpolate how the bezier control point should be adjusted.
+// These numbers were determined emperically by adjusting a chord and discovering
+// the relationship between the width of the inner bezier and the lengths of the arcs.
+// If we were just drawing the chord diagram once, we wouldn't need to use scales. 
+// But since we are animating chords, we need to smoothly chnage the control point from
+// [0, 0] to 1/2 way to the center of the bezier curve. 
+const dom = [.06, .98, Math.PI];
+const ys = d3.scale.linear().domain(dom).range([.18, 0, 0]);
+const x0s = d3.scale.linear().domain(dom).range([.03, .24, .24]);
+const x1s = d3.scale.linear().domain(dom).range([.24, .6, .6]);
+const x2s = d3.scale.linear().domain(dom).range([1.32, .8, .8]);
+const x3s = d3.scale.linear().domain(dom).range([3, 2, 2]);
+
+function qdrRibbon() { // eslint-disable-line no-unused-vars
+  var r = 200;  // random default. this will be set later
+
+  // This is the function that gets called to produce a path for a chord.
+  // The path should end up looking like 
+  // M[start point]A[arc options][arc end point]Q[control point][end points]A[arc options][arc end point]Q[control point][end points]Z
+  var ribbon = function (d) {
+    let sa0 = d.source.startAngle - halfPI,
+      sa1 = d.source.endAngle - halfPI,
+      ta0 = d.target.startAngle - halfPI,
+      ta1 = d.target.endAngle - halfPI;
+
+    // The control points for the bezier curves
+    let cp1 = [0, 0];
+    let cp2 = [0, 0];
+    // the span of the two arcs
+    let arc1 = Math.abs(sa0 - sa1);
+    let arc2 = Math.abs(ta0 - ta1);
+    let largeArc = Math.max(arc1, arc2);
+    let smallArc = Math.min(arc1, arc2);
+    // the gaps between the arcs
+    let gap1 = Math.abs(sa1 - ta0);
+    if (gap1 > Math.PI) gap1 = twoPI - gap1;
+    let gap2 = Math.abs(sa0 - ta1);
+    if (gap2 > Math.PI) gap2 = twoPI - gap2;
+    let sgap = Math.min(gap1, gap2);
+
+    // if the bezier curves intersect, ratiocp will be > 0
+    let ratiocp = cpRatio(sgap, largeArc, smallArc);
+
+    // x, y points for the start and end of the arcs
+    let s0x = r * Math.cos(sa0),
+      s0y = r * Math.sin(sa0),
+      t0x = r * Math.cos(ta0),
+      t0y = r * Math.sin(ta0);
+    
+    if (ratiocp > 0) {
+      // determine which control point to calculate
+      if ((Math.abs(gap1-gap2) < 1e-2) || (gap1 < gap2)) {
+        let s1x = r * Math.cos(sa1),
+          s1y = r * Math.sin(sa1);
+        cp1 = [ratiocp*(s1x + t0x)/2, ratiocp*(s1y + t0y)/2];
+      } else {
+        let t1x = r * Math.cos(ta1),
+          t1y = r * Math.sin(ta1);
+        cp2 = [ratiocp*(t1x + s0x)/2, ratiocp*(t1y + s0y)/2];
+      }
+    }
+
+    // construct the path using the control points
+    let path = d3.path();
+    path.moveTo(s0x, s0y);
+    path.arc(0, 0, r, sa0, sa1);
+    if (sa0 != ta0 || sa1 !== ta1) {
+      path.quadraticCurveTo(cp1[0], cp1[1], t0x, t0y);
+      path.arc(0, 0, r, ta0, ta1);
+    }
+    path.quadraticCurveTo(cp2[0], cp2[1], s0x, s0y);
+    path.closePath();
+    return path + '';
+
+  };
+  ribbon.radius = function (radius) {
+    if (!arguments.length) return r;
+    r = radius;
+    return ribbon;
+  };
+  return ribbon;
+}
+
+let sqr = function (n) { return n * n; };
+let dist = function (p1x, p1y, p2x, p2y) { return sqr(p1x - p2x) + sqr(p1y - p2y);};
+// distance from a point to a line segment
+let distToLine = function (vx, vy, wx, wy, px, py) {
+  let vlen = dist(vx, vy, wx, wy);
+  if (vlen === 0) return dist(px, py, vx, vy);
+  var t = ((px-vx)*(wx-vx) + (py-vy)*(wy-vy)) / vlen;
+  t = Math.max(0, Math.min(1, t)); // clamp t to between 0 and 1
+  return Math.sqrt(dist(px, py, vx + t*(wx-vx), vy + t*(wy-vy)));
+};
+
+// See if x, y is contained in trapezoid.
+// gap is the smallest gap in the chord
+// x is the size of the longest arc
+// y is the size of the smallest arc
+// the trapezoid is defined by [x0, 0] [x1, top] [x2, top] [x3, 0]
+// these points are determined by the gap
+let cpRatio = function (gap, x, y) {
+  let top = ys(gap);
+  if (y >= top)
+    return 0;
+
+  // get the xpoints of the trapezoid
+  let x0 = x0s(gap);
+  if (x <= x0)
+    return 0;
+  let x3 = x3s(gap);
+  if (x > x3)
+    return 0;
+
+  let x1 = x1s(gap);
+  let x2 = x2s(gap);
+  
+  // see if the point is to the right of (inside) the leftmost diagonal
+  // compute the outer product of the left diagonal and the point
+  let op = (x-x0)*top - y*(x1-x0);
+  if (op <= 0)
+    return 0;
+  // see if the point is to the left of the right diagonal
+  op = (x-x3)*top - y*(x2-x3);
+  if (op >= 0)
+    return 0;
+
+  // the point is in the trapezoid. see how far in
+  let dist = 0;
+  if (x < x1) {
+    // left side. get distance to left diagonal
+    dist = distToLine(x0, 0, x1, top, x, y);
+  } else if (x > x2) {
+    // right side. get distance to right diagonal
+    dist = distToLine(x3, 0, x2, top, x, y);
+  } else {
+    // middle. get distance to top
+    dist = top - y;
+  }
+  let distScale = d3.scale.linear().domain([0, top/8, top/2, top]).range([0, .3, .4, .5]);
+  return distScale(dist);
+};

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/55e08784/console/stand-alone/plugin/js/dispatchPlugin.js
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/js/dispatchPlugin.js b/console/stand-alone/plugin/js/dispatchPlugin.js
index f5df9c4..1eb320f 100644
--- a/console/stand-alone/plugin/js/dispatchPlugin.js
+++ b/console/stand-alone/plugin/js/dispatchPlugin.js
@@ -74,7 +74,7 @@ var QDR = (function(QDR) {
    * This plugin's angularjs module instance
    */
   QDR.module = angular.module(QDR.pluginName, ['ngRoute', 'ngSanitize', 'ngResource', 'ui.bootstrap',
-    'ui.grid', 'ui.grid.selection', 'ui.grid.autoResize', 'ui.grid.resizeColumns', 'ui.grid.saveState', 'ui.slider']);
+    'ui.grid', 'ui.grid.selection', 'ui.grid.autoResize', 'ui.grid.resizeColumns', 'ui.grid.saveState', 'ui.slider', 'ui.checkbox']);
 
   // set up the routing for this plugin
   QDR.module.config(function($routeProvider) {
@@ -97,6 +97,9 @@ var QDR = (function(QDR) {
       .when('/charts', {
         templateUrl: QDR.templatePath + 'qdrCharts.html'
       })
+      .when('/chord', {
+        templateUrl: 'plugin/html/qdrChord.html'
+      })
       .when('/connect', {
         templateUrl: QDR.templatePath + 'qdrConnect.html'
       });

http://git-wip-us.apache.org/repos/asf/qpid-dispatch/blob/55e08784/console/stand-alone/plugin/js/navbar.js
----------------------------------------------------------------------
diff --git a/console/stand-alone/plugin/js/navbar.js b/console/stand-alone/plugin/js/navbar.js
index 24dcbc6..c127e85 100644
--- a/console/stand-alone/plugin/js/navbar.js
+++ b/console/stand-alone/plugin/js/navbar.js
@@ -61,6 +61,13 @@ var QDR = (function (QDR) {
       name: 'Charts'
     },
     {
+      content: '<i class="chord-diagram"></i> Message Flow',
+      title: 'Chord chart',
+      isValid: function (QDRService) { return QDRService.management.connection.is_connected(); },
+      href: '#/chord',
+      name: 'Message Flow'
+    },
+    {
       content: '<i class="icon-align-left"></i> Schema',
       title: 'View dispatch schema',
       isValid: function (QDRService) { return QDRService.management.connection.is_connected(); },


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