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