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 2019/09/26 19:03:45 UTC

[qpid-dispatch] branch eallen-DISPATCH-1385 updated: Topology map and message flow chord diagram

This is an automated email from the ASF dual-hosted git repository.

eallen pushed a commit to branch eallen-DISPATCH-1385
in repository https://gitbox.apache.org/repos/asf/qpid-dispatch.git


The following commit(s) were added to refs/heads/eallen-DISPATCH-1385 by this push:
     new 38fee58  Topology map and message flow chord diagram
38fee58 is described below

commit 38fee584402deeb4b1273f6b3f32453326771717
Author: Ernest Allen <ea...@redhat.com>
AuthorDate: Thu Sep 26 15:03:22 2019 -0400

    Topology map and message flow chord diagram
---
 console/react/public/data/countries.json           |    2 +
 console/react/src/App.css                          |  133 +-
 console/react/src/amqp/topology.js                 |   12 +-
 console/react/src/chord/addressesComponent.js      |   86 +
 console/react/src/chord/layout/layout.js           |   81 +-
 .../src/{topology => chord}/legendComponent.js     |   88 +-
 console/react/src/chord/optionsComponent.js        |   53 +
 console/react/src/chord/qdrChord.js                | 1666 +++++++++++---------
 console/react/src/chord/ribbon/ribbon.js           |  107 +-
 console/react/src/chord/routersComponent.js        |   56 +
 console/react/src/layout.js                        |    7 +-
 console/react/src/topology/legend.js               |   49 +-
 console/react/src/topology/legendComponent.js      |    8 +-
 console/react/src/topology/map.js                  |  216 ++-
 console/react/src/topology/mapLegendComponent.jsx  |   94 ++
 console/react/src/topology/qdrTopology.js          |   17 +-
 console/react/src/topology/traffic.js              |    5 +-
 17 files changed, 1638 insertions(+), 1042 deletions(-)

diff --git a/console/react/public/data/countries.json b/console/react/public/data/countries.json
new file mode 100644
index 0000000..8b17350
--- /dev/null
+++ b/console/react/public/data/countries.json
@@ -0,0 +1,2 @@
+
+{"type":"Topology","objects":{"countries":{"type":"GeometryCollection","geometries":[{"type":"Polygon","id":"AFG","properties":{"name":"Afghanistan"},"arcs":[[0,1,2,3,4,5]]},{"type":"MultiPolygon","id":"AGO","properties":{"name":"Angola"},"arcs":[[[6,7,8,9]],[[10,11,12]]]},{"type":"Polygon","id":"ALB","properties":{"name":"Albania"},"arcs":[[13,14,15,16,17,18,19,20]]},{"type":"Polygon","id":"ALD","properties":{"name":"Aland"},"arcs":[[21]]},{"type":"Polygon","id":"AND","properties":{"nam [...]
\ No newline at end of file
diff --git a/console/react/src/App.css b/console/react/src/App.css
index f2a7a12..1d89e47 100644
--- a/console/react/src/App.css
+++ b/console/react/src/App.css
@@ -379,7 +379,8 @@ div.state-container button.pf-c-clipboard-copy__group-copy {
   width: 100%;
 }
 
-.qdrTopology dl.pf-c-accordion {
+.qdrTopology dl.pf-c-accordion,
+.qdrChord dl.pf-c-accordion {
   position: absolute;
   width: 20em;
   right: 1em;
@@ -388,16 +389,19 @@ div.state-container button.pf-c-clipboard-copy__group-copy {
   margin-top: 1em;
 }
 
-.qdrTopology dl.pf-c-accordion * {
+.qdrTopology dl.pf-c-accordion *,
+.qdrChord dl.pf-c-accordion * {
   border-left-color: transparent;
 }
 
-.qdrTopology dl.pf-c-accordion dt {
+.qdrTopology dl.pf-c-accordion dt,
+.qdrChord dl.pf-c-accordion dt {
   background-image: linear-gradient(to bottom, #fafafa 0, #ededed 100%);
   background-repeat: repeat-x;
 }
 
-.qdrTopology dl.pf-c-accordion h3 {
+.qdrTopology dl.pf-c-accordion h3,
+.qdrChord dl.pf-c-accordion h3 {
   margin-top: 0;
   margin-bottom: 0;
 }
@@ -430,7 +434,8 @@ g text {
   pointer-events: none;
 }
 
-svg.address-svg g text {
+svg.address-svg g text,
+svg.chord-address-svg g text {
   cursor: pointer;
   pointer-events: auto;
 }
@@ -597,18 +602,28 @@ svg#svglegend {
 #traffic-expand,
 #map-expand,
 #arrows-expand {
-  max-height: 20em;
+  max-height: 23em;
 }
 
-div.qdrTopology {
+div.qdrTopology,
+div.qdrChord {
   text-align: left;
 }
 
 #arrows-expand label,
 #traffic-dots label,
-#traffic-congestion label {
+#traffic-congestion label,
+#options-expand label {
   position: relative;
   top: 4px;
+  padding-left: 0.25em;
+}
+
+#arrows-expand .pf-c-check {
+  padding-top: 0.5em;
+}
+.map-legend label {
+  padding-left: 1em;
 }
 
 #traffic-address .pf-c-check {
@@ -621,3 +636,105 @@ div.qdrTopology {
 .popup-info table.popupTable td {
   font-size: 14px;
 }
+
+circle.flow {
+  pointer-events: none;
+}
+/* for message flow chord diagram */
+path.chord {
+  fill-opacity: 0.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;
+}
+
+path.empty {
+  fill: rgb(31, 119, 180);
+  fill-opacity: 0.67;
+}
+
+.addresses .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: 0.95;
+  padding: 12px;
+  font-size: 14px;
+}
+
+#chord text {
+  pointer-events: all;
+}
+
+#noTraffic {
+  position: relative;
+  top: 3em;
+  left: 5em;
+}
+
+.page-menu {
+  position: absolute;
+  right: 1em;
+  top: 1em;
+  left: auto;
+  width: auto;
+}
+
+@media (max-width: 768px) {
+  .navbar-collapse.page-menu {
+    left: 0;
+    top: 0;
+    right: auto;
+  }
+}
+
+.navbar-collapse {
+  padding-right: 0;
+}
+
+.panel-group {
+  margin-bottom: 0;
+}
+
+span.legend-color {
+  width: 20px;
+  height: 20px;
+  display: inline-block;
+  margin-right: 0.5em;
+}
+
+span.legend-router {
+  white-space: nowrap;
+  position: relative;
+  top: -5px;
+}
+
+div.qdrChord .legend-text {
+  color: black;
+  font-size: 16px;
+}
diff --git a/console/react/src/amqp/topology.js b/console/react/src/amqp/topology.js
index 9b467b0..0e8b2e4 100644
--- a/console/react/src/amqp/topology.js
+++ b/console/react/src/amqp/topology.js
@@ -548,18 +548,18 @@ class Topology {
     newResponse.attributeNames = thisNode.attributeNames;
     newResponse.results = thisNode.results;
     newResponse.aggregates = [];
+    const addVal = (vals, val) => {
+      vals.push({ sum: val, detail: [] });
+    };
     // initialize the aggregates
     for (var i = 0; i < thisNode.results.length; ++i) {
       // there is a result for each unique entity found (ie addresses, links, etc.)
       var result = thisNode.results[i];
       var vals = [];
       // there is a val for each attribute in this entity
-      result.forEach(function(val) {
-        vals.push({
-          sum: val,
-          detail: []
-        });
-      });
+      for (let i = 0; i < result.length; i++) {
+        addVal(vals, result[i]);
+      }
       newResponse.aggregates.push(vals);
     }
     var nameIndex = thisNode.attributeNames.indexOf("name");
diff --git a/console/react/src/chord/addressesComponent.js b/console/react/src/chord/addressesComponent.js
new file mode 100644
index 0000000..e0333c2
--- /dev/null
+++ b/console/react/src/chord/addressesComponent.js
@@ -0,0 +1,86 @@
+/*
+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.
+*/
+
+import React, { Component } from "react";
+
+class AddressesComponent extends Component {
+  constructor(props) {
+    super(props);
+    this.state = {};
+  }
+
+  dotClicked = address => {
+    this.props.handleChangeAddress(address, !this.props.addresses[address]);
+  };
+
+  dotHover = (address, over) => {
+    this.props.handleHoverAddress(address, over);
+  };
+
+  coloredDot = (address, i) => {
+    return (
+      <svg
+        className="chord-address-svg"
+        id={`address-dot-${i}`}
+        width="200"
+        height="20"
+        title="Click to show/hide this address"
+      >
+        <g
+          transform="translate(10,10)"
+          onClick={() => this.dotClicked(address)}
+          onMouseOver={() => this.dotHover(address, true)}
+          onMouseOut={() => this.dotHover(address, false)}
+        >
+          <circle r="10" fill={this.props.chordColors[address]} />
+          {this.props.addresses[address] ? (
+            <text x="-8" y="5" className="address-checkbox">
+              &#xf00c;
+            </text>
+          ) : (
+            ""
+          )}
+          <text x="20" y="5" className="label">
+            {address}
+          </text>
+        </g>
+      </svg>
+    );
+  };
+
+  render() {
+    return (
+      <ul className="addresses">
+        {Object.keys(this.props.addresses).length === 0 ? (
+          <li key={`address-empty`}>There is no traffic</li>
+        ) : (
+          Object.keys(this.props.addresses).map((address, i) => {
+            return (
+              <li key={`address-${i}`} className="legend-line">
+                {this.coloredDot(address, i)}
+              </li>
+            );
+          })
+        )}
+      </ul>
+    );
+  }
+}
+
+export default AddressesComponent;
diff --git a/console/react/src/chord/layout/layout.js b/console/react/src/chord/layout/layout.js
index 907e9a8..9f8196d 100644
--- a/console/react/src/chord/layout/layout.js
+++ b/console/react/src/chord/layout/layout.js
@@ -16,24 +16,42 @@ KIND, either express or implied.  See the License for the
 specific language governing permissions and limitations
 under the License.
 */
-/* global d3 */
+import * as d3 from "d3";
 
-var qdrlayoutChord = function() { // eslint-disable-line no-unused-vars
-  var chord = {}, chords, groups, matrix, n, padding = 0, τ = Math.PI*2, groupBy;
+var qdrlayoutChord = 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;
+    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;
+    k = 0;
+    i = -1;
     while (++i < n) {
-      x = 0, j = -1;
+      x = 0;
+      j = -1;
       while (++j < n) {
         x += matrix[i][j];
       }
@@ -43,7 +61,9 @@ var qdrlayoutChord = function() { // eslint-disable-line no-unused-vars
     // the fraction of the circle for each incremental value
     k = (τ - padding * groupLen) / k;
     // for each row
-    x = 0, i = -1, ldi = groupBy[0];
+    x = 0;
+    i = -1;
+    ldi = groupBy[0];
     while (++i < n) {
       di = groupBy[i];
       // insert padding after each group
@@ -52,11 +72,15 @@ var qdrlayoutChord = function() { // eslint-disable-line no-unused-vars
         ldi = di;
       }
       // for each column
-      x0 = x, j = -1;
+      x0 = x;
+      j = -1;
       while (++j < n) {
-        var dj = groupBy[j], v = matrix[i][j], a0 = x, a1 = x += v * k;
+        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] = {
+        subgroups[i + "-" + j] = {
           index: di,
           subindex: dj,
           orgindex: i,
@@ -85,16 +109,21 @@ var qdrlayoutChord = function() { // eslint-disable-line no-unused-vars
     while (++i < n) {
       j = i - 1;
       while (++j < n) {
-        var source = subgroups[i + '-' + j], target = subgroups[j + '-' + i];
+        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
-          });
+          chords.push(
+            source.value < target.value
+              ? {
+                  source: target,
+                  target: source
+                }
+              : {
+                  source: source,
+                  target: target
+                }
+          );
         }
       }
     }
@@ -111,7 +140,7 @@ var qdrlayoutChord = function() { // eslint-disable-line no-unused-vars
     chords = groups = null;
     return chord;
   };
-  chord.groupBy = function (x) {
+  chord.groupBy = function(x) {
     if (!arguments.length) return groupBy;
     groupBy = x;
     chords = groups = null;
@@ -128,15 +157,15 @@ var qdrlayoutChord = function() { // eslint-disable-line no-unused-vars
   return chord;
 };
 
-let fill = function (value, length) {
-  var i=0, array = []; 
-  array.length = length; 
-  while(i < length) 
-    array[i++] = value;
+let fill = function(value, length) {
+  var i = 0,
+    array = [];
+  array.length = length;
+  while (i < length) array[i++] = value;
   return array;
 };
 
-let unique = function (arr) {
+let unique = function(arr) {
   var counts = {};
   for (var i = 0; i < arr.length; i++) {
     counts[arr[i]] = 1 + (counts[arr[i]] || 0);
@@ -144,4 +173,4 @@ let unique = function (arr) {
   return Object.keys(counts).length;
 };
 
-export { qdrlayoutChord };
\ No newline at end of file
+export { qdrlayoutChord };
diff --git a/console/react/src/topology/legendComponent.js b/console/react/src/chord/legendComponent.js
similarity index 50%
copy from console/react/src/topology/legendComponent.js
copy to console/react/src/chord/legendComponent.js
index 20795d3..a9a2901 100644
--- a/console/react/src/topology/legendComponent.js
+++ b/console/react/src/chord/legendComponent.js
@@ -24,9 +24,9 @@ import {
   AccordionContent,
   AccordionToggle
 } from "@patternfly/react-core";
-
-import ArrowsComponent from "./arrowsComponent";
-import TrafficComponent from "./trafficComponent";
+import OptionsComponent from "./optionsComponent";
+import RoutersComponent from "./routersComponent";
+import AddressesComponent from "./addressesComponent";
 
 class LegendComponent extends Component {
   constructor(props) {
@@ -44,81 +44,61 @@ class LegendComponent extends Component {
       <Accordion>
         <AccordionItem>
           <AccordionToggle
-            onClick={() => toggle("traffic")}
-            isExpanded={this.props.trafficOpen}
-            id="traffic"
+            onClick={() => toggle("options")}
+            isExpanded={this.props.optionsOpen}
+            id="options"
           >
-            Traffic
+            Options
           </AccordionToggle>
           <AccordionContent
-            id="traffic-expand"
-            isHidden={!this.props.trafficOpen}
+            id="options-expand"
+            isHidden={!this.props.optionsOpen}
             isFixed
           >
-            <TrafficComponent
-              addresses={this.props.addresses}
-              addressColors={this.props.addressColors}
-              open={this.props.trafficOpen}
-              handleChangeTrafficAnimation={
-                this.props.handleChangeTrafficAnimation
-              }
-              handleChangeTrafficFlowAddress={
-                this.props.handleChangeTrafficFlowAddress
-              }
-              dots={this.props.dots}
-              congestion={this.props.congestion}
+            <OptionsComponent
+              isRate={this.props.isRate}
+              byAddress={this.props.byAddress}
+              handleChangeOption={this.props.handleChangeOption}
             />
           </AccordionContent>
         </AccordionItem>
         <AccordionItem>
           <AccordionToggle
-            onClick={() => toggle("legend")}
-            isExpanded={this.props.legendOpen}
-            id="legend"
+            onClick={() => toggle("routers")}
+            isExpanded={this.props.routersOpen}
+            id="routers"
           >
-            Legend
+            Routers
           </AccordionToggle>
           <AccordionContent
-            id="legend-expand"
-            isHidden={!this.props.legendOpen}
+            id="routers-expand"
+            isHidden={!this.props.routersOpen}
             isFixed
           >
-            <div id="topo_svg_legend"></div>
-          </AccordionContent>
-        </AccordionItem>
-        <AccordionItem>
-          <AccordionToggle
-            onClick={() => toggle("map")}
-            isExpanded={this.props.mapOpen}
-            id="map"
-          >
-            Background map
-          </AccordionToggle>
-          <AccordionContent
-            id="map-expand"
-            isHidden={!this.props.mapOpen}
-            isFixed
-          >
-            <p>Map options go here</p>
+            <RoutersComponent
+              arcColors={this.props.arcColors}
+              handleHoverRouter={this.props.handleHoverRouter}
+            />
           </AccordionContent>
         </AccordionItem>
         <AccordionItem>
           <AccordionToggle
-            onClick={() => toggle("arrows")}
-            isExpanded={this.props.arrowsOpen}
-            id="arrows"
+            onClick={() => toggle("addresses")}
+            isExpanded={this.props.addressesOpen}
+            id="addresses"
           >
-            Arrows
+            Addresses
           </AccordionToggle>
           <AccordionContent
-            id="arrows-expand"
-            isHidden={!this.props.arrowsOpen}
+            id="addresses-expand"
+            isHidden={!this.props.addressesOpen}
             isFixed
           >
-            <ArrowsComponent
-              handleChangeArrows={this.props.handleChangeArrows}
-              routerArrows={this.props.routerArrows}
-              clientArrows={this.props.clientArrows}
+            <AddressesComponent
+              addresses={this.props.addresses}
+              chordColors={this.props.chordColors}
+              handleChangeAddress={this.props.handleChangeAddress}
+              handleHoverAddress={this.props.handleHoverAddress}
             />
           </AccordionContent>
         </AccordionItem>
diff --git a/console/react/src/chord/optionsComponent.js b/console/react/src/chord/optionsComponent.js
new file mode 100644
index 0000000..c02120b
--- /dev/null
+++ b/console/react/src/chord/optionsComponent.js
@@ -0,0 +1,53 @@
+/*
+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.
+*/
+
+import React, { Component } from "react";
+import { Checkbox } from "@patternfly/react-core";
+
+class OptionsComponent extends Component {
+  constructor(props) {
+    super(props);
+    this.state = {};
+  }
+
+  render() {
+    return (
+      <React.Fragment>
+        <Checkbox
+          label="Show rates"
+          isChecked={this.props.isRate}
+          onChange={this.props.handleChangeOption}
+          aria-label="show rates"
+          id="check-rates"
+          name="isRate"
+        />
+        <Checkbox
+          label="Show by address"
+          isChecked={this.props.byAddress}
+          onChange={this.props.handleChangeOption}
+          aria-label="show by address"
+          id="check-address"
+          name="byAddress"
+        />
+      </React.Fragment>
+    );
+  }
+}
+
+export default OptionsComponent;
diff --git a/console/react/src/chord/qdrChord.js b/console/react/src/chord/qdrChord.js
index edb1b3a..24a5caf 100644
--- a/console/react/src/chord/qdrChord.js
+++ b/console/react/src/chord/qdrChord.js
@@ -16,827 +16,973 @@ KIND, either express or implied.  See the License for the
 specific language governing permissions and limitations
 under the License.
 */
-/* global angular d3 */
-
-import { QDRRedirectWhenConnected } from '../qdrGlobals.js';
-import { separateAddresses, aggregateAddresses } from './filters.js';
-import { ChordData } from './data.js';
-import { qdrRibbon } from './ribbon/ribbon.js';
-import { qdrlayoutChord } from './layout/layout.js';
-
-export class ChordController {
-  constructor(QDRService, $scope, $location, $timeout, $sce) {
-    this.controllerName = 'QDR.ChordController';
-    // 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()) {
-      QDRRedirectWhenConnected($location, 'chord');
-      return;
-    }
-
-    const CHORDOPTIONSKEY = 'chordOptions';
-    const CHORDFILTERKEY =  'chordFilter';
-    const DOUGHNUT =        '#chord svg .empty';
-    const ERROR_RENDERING = 'Error while rendering ';
-    const ARCPADDING = .06;
-    const SMALL_OFFSET = 210;
-    const MIN_RADIUS = 200;
-
-    // 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 = {};
-
-    $scope.legend = {status: {addressesOpen: true, routersOpen: true, optionsOpen: true}};
-    // 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), MIN_RADIUS);
+import React, { Component } from "react";
+import * as d3 from "d3";
+//import { QDRRedirectWhenConnected } from "../qdrGlobals.js";
+import { separateAddresses, aggregateAddresses } from "./filters.js";
+import { ChordData } from "./data.js";
+import { qdrRibbon } from "./ribbon/ribbon.js";
+import { qdrlayoutChord } from "./layout/layout.js";
+import LegendComponent from "./legendComponent";
+import QDRPopup from "../qdrPopup";
+
+const CHORDOPTIONSKEY = "chordOptions";
+const CHORDFILTERKEY = "chordFilter";
+const DOUGHNUT = "#chord svg .empty";
+const ERROR_RENDERING = "Error while rendering ";
+const ARCPADDING = 0.06;
+const SMALL_OFFSET = 210;
+const MIN_RADIUS = 200;
+const TRANSITION_DURATION = 1000;
+
+class MessageFlowPage extends Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      addresses: [],
+      showPopup: false,
+      popupContent: "",
+      showEmpty: false,
+      emptyText: ""
     };
-
-    // 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();
-
-    $scope.navbutton_toggle = function () {
-      let legendPos = $('#legend').position();
-      console.log(legendPos);
-      if (legendPos.left === 0)
-        setTimeout(windowResized, 10);
-      else
-        $('#switches').css({left: -legendPos.left, opacity: 1});
-    };
-    // TODO: handle window resizes
-    //let updateWindow  = function () {
-    //setSizes();
-    //startOver();
-    //};
-    //d3.select(window).on('resize.updatesvg', updateWindow);
-    let windowResized = function () {
-      let legendPos = $('#legend').position();
-      let switches = $('#switches');
-      let outerWidth = switches.outerWidth();
-      if (switches && legendPos)
-        switches.css({left: (legendPos.left - outerWidth), opacity: 1});
-    };
-    window.addEventListener('resize', function () {
-      windowResized();
-      setTimeout(windowResized, 1);
-    });
-
-    // used for animation duration and the data refresh interval 
-    let transitionDuration = 1000;
+    let savedOptions = localStorage.getItem(CHORDFILTERKEY);
+    this.excludedAddresses = savedOptions ? JSON.parse(savedOptions) : [];
+    savedOptions = localStorage.getItem(CHORDOPTIONSKEY);
+    this.state.legendOptions = savedOptions
+      ? JSON.parse(savedOptions)
+      : {
+          isRate: true,
+          byAddress: true,
+          optionsOpen: true,
+          routersOpen: true,
+          addressesOpen: true
+        };
+    if (typeof this.state.legendOptions.optionsOpen === "undefined") {
+      this.state.legendOptions["optionsOpen"] = true;
+      this.state.legendOptions["routersOpen"] = true;
+      this.state.legendOptions["addressesOpen"] = true;
+    }
+    this.chordData = new ChordData(
+      this.props.service,
+      this.state.legendOptions.isRate,
+      this.state.legendOptions.byAddress
+        ? separateAddresses
+        : aggregateAddresses
+    );
+    this.chordData.setFilter(this.excludedAddresses);
+    this.chordColors = {};
+    this.outerRadius = null;
+    this.innerRadius = null;
+    this.textRadius = null;
+
+    // used for animation duration and the data refresh interval
     // format with commas
-    let formatNumber = d3.format(',.1f');
+    this.formatNumber = d3.format(",.1f");
 
     // colors
-    let colorGen = d3.scale.category20();
-    // The colorGen funtion is not random access. 
+    this.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);
+    for (let i = 0; i < 20; i++) {
+      this.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;
+    this.last_chord = null;
+    this.last_labels = null;
 
     // global pointer to the diagram
-    let svg;
-
-    // called once when the page loads
-    let initSvg = function () {
-      d3.select('#chord svg').remove();
-
-      let xtrans = outerRadius === MIN_RADIUS ? SMALL_OFFSET : outerRadius;
-      svg = d3.select('#chord').append('svg')
-        .attr('width', outerRadius * 2)
-        .attr('height', outerRadius * 2)
-        .append('g')
-        .attr('id', 'circle')
-        .attr('transform', 'translate(' + xtrans + ',' + 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();
+    this.svg = null;
 
-    let emptyCircle = function () {
-      $scope.noValues = false;
-      d3.select(DOUGHNUT).remove();
+    this.popoverChord = null;
+    this.popoverArc = null;
+    this.theyveBeenWarned = false;
+    this.arcColors = {};
+  }
 
-      let arc = d3.svg.arc()
-        .innerRadius(innerRadius)
-        .outerRadius(textRadius)
-        .startAngle(0)
-        .endAngle(Math.PI * 2);
+  // called only once when the component is initialized
+  componentDidMount() {
+    this.init();
+  }
 
-      d3.select('#circle').append('path')
-        .attr('class', 'empty')
-        .attr('d', arc);
-    };
+  componentWillUnmount() {
+    // stop updated the data
+    clearInterval(this.interval);
+    // clean up memory associated with the svg
+    //d3.select("#chord").remove();
+    //d3.select(window).on("resize.updatesvg", null);
+    //window.removeEventListener("resize", this.windowResized);
+  }
 
-    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);
-    };
+  init = () => {
+    this.setSizes();
 
-    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;
-    };
+    // used to transition chords along a circular path instead of linear.
+    // qdrRibbon is a replacement for d3.svg.chord() that avoids the twists
+    this.chordReference = qdrRibbon().radius(this.innerRadius);
+    //this.chordReference = d3.svg.chord().radius(this.innerRadius);
 
-    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);
+    // used to transition arcs along a curcular path instead of linear
+    this.arcReference = d3.svg
+      .arc()
+      .startAngle(d => {
+        return d.startAngle;
+      })
+      .endAngle(d => {
+        return d.endAngle;
+      })
+      .innerRadius(this.innerRadius)
+      .outerRadius(this.textRadius);
+
+    d3.select("#chord svg").remove();
+
+    let xtrans =
+      this.outerRadius === MIN_RADIUS ? SMALL_OFFSET : this.outerRadius;
+    this.svg = d3
+      .select("#chord")
+      .append("svg")
+      .attr("width", this.outerRadius * 2)
+      .attr("height", this.outerRadius * 2)
+      .append("g")
+      .attr("id", "circle")
+      .attr("transform", `translate(${xtrans},${this.outerRadius})`);
+
+    // mouseover target for when the mouse leaves the diagram
+    this.svg
+      .append("circle")
+      .attr("r", this.innerRadius * 2)
+      .on("mouseover", this.showAllChords);
+
+    // background circle. will only get a mouseover event if the mouse is between chords
+    this.svg
+      .append("circle")
+      .attr("r", this.innerRadius)
+      .on("mouseover", () => {
+        d3.event.stopPropagation();
       });
-      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;
-      }
+    this.svg = this.svg.append("g").attr("class", "chart-container");
+    window.addEventListener("resize", () => {
+      this.windowResized();
+      setTimeout(this.windowResized, 1);
+    });
 
-      $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';
-            let autoHide = outerRadius === MIN_RADIUS;
-            $.notify($('#noTraffic'), msg, {clickToHide: autoHide, autoHide: autoHide, arrowShow: false, className: 'Warning'});
-            $('.notifyjs-wrapper').css('z-index', autoHide ? 3 : 0);
-          }
-        });
-        emptyCircle();
-        matrixMessages = [];
-      } else {
-        matrixMessages = matrix.matrixMessages();
-        $('.notifyjs-wrapper').hide();
-        theyveBeenWarned = false;
-        fadeDoughnut();
+    // get the raw data and render the svg
+    this.chordData.getMatrix().then(
+      matrix => {
+        // now that we have the routers and addresses, move the legend
+        this.windowResized();
+        this.renderChord(matrix);
+      },
+      e => {
+        console.log(ERROR_RENDERING + e);
       }
+    );
+
+    this.interval = setInterval(this.doUpdate, TRANSITION_DURATION);
+  };
+
+  navbutton_toggle = () => {
+    let legendPos = d3
+      .select("#legend")
+      .node()
+      .position();
+    if (legendPos.left === 0) setTimeout(this.windowResized, 10);
+  };
+  // TODO: handle window resizes
+  //let updateWindow  = function () {
+  //setSizes();
+  //startOver();
+  //};
+  //d3.select(window).on('resize.updatesvg', updateWindow);
+  windowResized = () => {};
+
+  // size the diagram based on the browser window size
+  getRadius = () => {
+    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), MIN_RADIUS);
+  };
+
+  // diagram sizes that change when browser is resized
+  setSizes = () => {
+    // size of circle + text
+    this.outerRadius = this.getRadius();
+    // size of chords
+    this.innerRadius = this.outerRadius - 130;
+    // arc ring around chords
+    this.textRadius = Math.min(this.innerRadius * 1.1, this.innerRadius + 15);
+  };
+
+  // arc colors are taken from every other color starting at 0
+  getArcColor = n => {
+    if (!(n in this.arcColors)) {
+      let ci = Object.keys(this.arcColors).length * 2;
+      this.arcColors[n] = this.colorGen(ci);
+    }
+    return this.arcColors[n];
+  };
+  // chord colors are taken from every other color starting at 19 and going backwards
+  getChordColor = n => {
+    if (!(n in this.chordColors)) {
+      let ci = 19 - Object.keys(this.chordColors).length * 2;
+      let c = this.colorGen(ci);
+      this.chordColors[n] = c;
+    }
+    return this.chordColors[n];
+  };
+
+  // fade out the empty circle that is shown when there is no traffic
+  fadeDoughnut = () => {
+    d3.select(DOUGHNUT)
+      .transition()
+      .duration(200)
+      .attr("opacity", 0)
+      .remove();
+  };
+
+  // return the color associated with a router
+  fillArc = (matrixValues, row) => {
+    let router = matrixValues.routerName(row);
+    return this.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
+  fillChord = (matrixValues, d) => {
+    // aggregate
+    if (matrixValues.aggregate) {
+      return this.fillArc(matrixValues, d.source.index);
+    }
+    // by address
+    let addr = matrixValues.getAddress(d.source.orgindex, d.source.orgsubindex);
+    return this.getChordColor(addr);
+  };
+
+  emptyCircle = () => {
+    d3.select(DOUGHNUT).remove();
+
+    let arc = d3.svg
+      .arc()
+      .innerRadius(this.innerRadius)
+      .outerRadius(this.textRadius)
+      .startAngle(0)
+      .endAngle(Math.PI * 2);
+
+    d3.select("#circle")
+      .append("path")
+      .attr("class", "empty")
+      .attr("d", arc);
+
+    this.setState({ noValues: false });
+  };
+
+  genArcColors = () => {
+    //$scope.arcColors = {};
+    let routers = this.chordData.getRouters();
+    routers.forEach(router => {
+      this.getArcColor(router);
+    });
+  };
+  genChordColors = () => {
+    if (this.state.legendOptions.byAddress) {
+      Object.keys(this.state.addresses).forEach(address => {
+        // populate this.chordColor for this address
+        this.getChordColor(address);
+      });
+    }
+  };
+  chordKey = (d, matrix) => {
+    // sort so that if the soure and target are flipped, the chord doesn't
+    // get destroyed and recreated
+    return this.getRouterNames(d, matrix)
+      .sort()
+      .join("-");
+  };
+
+  getRouterNames = (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 = !typeof d.index === "undefined" ? d.index : d.source.index;
+    let iindex = !typeof d.index === "undefined" ? 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
+  chordTitle = (d, matrix) => {
+    let rinfo = this.getRouterNames(d, matrix);
+    let from = rinfo[0],
+      to = rinfo[1],
+      address = rinfo[2];
+    if (!matrix.aggregate) {
+      address += "<br/>";
+    }
+    let title =
+      address + from + " → " + to + ": " + this.formatNumber(d.source.value);
+    if (d.target.value > 0 && to !== from) {
+      title +=
+        "<br/>" + to + " → " + from + ": " + this.formatNumber(d.target.value);
+    }
+    return title;
+  };
+  arcTitle = (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 + ": " + this.formatNumber(value);
+  };
+
+  decorateChordData = (rechord, matrix) => {
+    let data = rechord.chords();
+    data.forEach((d, i) => {
+      d.key = this.chordKey(d, matrix, false);
+      d.orgIndex = i;
+      d.color = this.fillChord(matrix, d);
+    });
+    return data;
+  };
+
+  decorateArcData = (fn, matrix) => {
+    let fixedGroups = fn();
+    fixedGroups.forEach(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 = this.getArcColor(fg.router);
+    });
+    return fixedGroups;
+  };
+
+  // create and/or update the chord diagram
+  renderChord = matrix => {
+    this.setState(
+      { addresses: this.chordData.getAddresses() },
+      this.doRenderChord(matrix)
+    );
+  };
+  doRenderChord = matrix => {
+    // populate the arcColors object with a color for each router
+    this.genArcColors();
+    this.genChordColors();
+    // if all the addresses are excluded, update the message
+    let addressLen = Object.keys(this.state.addresses).length;
+    this.allAddressesFiltered = false;
+    if (addressLen > 0 && this.excludedAddresses.length === addressLen) {
+      this.allAddressesFiltered = true;
+    }
 
-      // create a new chord layout so we can animate between the last one and this one
-      let groupBy = matrix.getGroupBy();
-      let rechord = qdrlayoutChord().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);
+    this.noValues = false;
+    let matrixMessages,
+      duration = TRANSITION_DURATION;
+
+    // if there is no data, show an empty circle and a message
+    if (!matrix.hasValues()) {
+      this.noValues = this.arcColors.length === 0;
+      if (!this.theyveBeenWarned) {
+        this.theyveBeenWarned = true;
+        let msg = "There is no message traffic";
+        if (addressLen !== 0) msg += " for the selected addresses";
+        this.setState({ showEmpty: true, emptyText: msg });
+      }
+      this.emptyCircle();
+      matrixMessages = [];
+    } else {
+      matrixMessages = matrix.matrixMessages();
+      this.setState({ showEmpty: false });
+      this.theyveBeenWarned = false;
+      this.fadeDoughnut();
+    }
 
-      // join the decorated data with a d3 selection
-      let arcsGroup = svg.selectAll('g.arc')
-        .data(rechord.arcData, function (d) {return d.key;});
+    // create a new chord layout so we can animate between the last one and this one
+    let groupBy = matrix.getGroupBy();
+    let rechord = qdrlayoutChord()
+      .padding(ARCPADDING)
+      .groupBy(groupBy)
+      .matrix(matrixMessages);
 
-      // get a d3 selection of all the new arcs that have been added
-      let newArcs = arcsGroup.enter().append('svg:g')
-        .attr('class', 'arc');
+    // 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 = this.decorateArcData(rechord.groups, matrix);
 
-      // 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; });
+    // join the decorated data with a d3 selection
+    let arcsGroup = this.svg.selectAll("g.arc").data(rechord.arcData, d => {
+      return d.key;
+    });
 
-      newArcs.append('svg:text')
-        .attr('dy', '.35em')
-        .text(function (d) {
-          return d.router;
-        });
+    // 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", d => d.color)
+      .style("stroke", d => d.color);
+
+    newArcs
+      .append("svg:text")
+      .attr("dy", ".35em")
+      .text(d => d.router);
+
+    // attach event listeners to all arcs (new or old)
+    arcsGroup
+      .on("mouseover", this.mouseoverArc)
+      .on("mousemove", d => {
+        let popupContent = this.arcTitle(d, matrix);
+        this.displayTooltip(d3.event, popupContent);
+      })
+      .on("mouseout", () => {
+        this.popoverArc = null;
+        this.setState({ showPopup: false });
+      });
 
-      // 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", this.arcTween(this.last_chord));
+    arcsGroup
+      .select("text")
+      .attr("text-anchor", d => (d.angle > Math.PI ? "end" : "begin"))
+      .transition()
+      .duration(duration)
+      .attrTween("transform", this.tickTween(this.last_labels));
+    // check if the mouse is hovering over an arc. if so, update the tooltip
+    arcsGroup.each(d => {
+      if (this.popoverArc && this.popoverArc.index === d.index) {
+        //let popoverContent = this.arcTitle(d, matrix);
+        //this.displayTooltip(d3.event, popoverContent);
+      }
+    });
 
-      // 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));
+    // animate the removal of any arcs that went away
+    let exitingArcs = arcsGroup.exit();
+    exitingArcs
+      .selectAll("text")
+      .transition()
+      .duration(duration / 2)
+      .attrTween("opacity", () => {
+        return t => {
+          return 1 - t;
+        };
+      });
 
-      // 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));
-          }
-        });
+    exitingArcs
+      .selectAll("path")
+      .transition()
+      .duration(duration / 2)
+      .attrTween("d", this.arcTweenExit)
+      .each("end", function() {
+        d3.select(this)
+          .node()
+          .parentNode.remove();
+      });
 
-      // animate the removal of any arcs that went away
-      let exitingArcs = arcsGroup.exit();
+    // decorate the chord layout's .chord() data with key, color, and orgIndex
+    rechord.chordData = this.decorateChordData(rechord, matrix);
+    let chordPaths = this.svg
+      .selectAll("path.chord")
+      .data(rechord.chordData, d => d.key);
 
-      exitingArcs.selectAll('text')
-        .transition()
-        .duration(duration/2)
-        .attrTween('opacity', function () {return function (t) {return 1 - t;};});
+    // new chords are paths
+    chordPaths
+      .enter()
+      .append("path")
+      .attr("class", "chord");
 
-      exitingArcs.selectAll('path')
-        .transition()
-        .duration(duration/2)
-        .attrTween('d', arcTweenExit)
-        .each('end', function () {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
+    if (!this.switchedByAddress) {
+      // do multiple concurrent tweens on the chords
       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
+        .call(this.tweenChordEnds, duration, this.last_chord)
+        .call(this.tweenChordColor, duration, this.last_chord, "stroke")
+        .call(this.tweenChordColor, duration, this.last_chord, "fill");
+    } else {
+      // switchByAddress is only true when we have new 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');
+        .attr("d", d => {
+          return this.chordReference(d);
         })
-        .on('mouseout', function () {
-          popoverChord = null;
-          d3.select('#popover-div')
-            .style('display', 'none');
-        });
-
-      let exitingChords = chordPaths.exit()
-        .attr('class', 'exiting-chord');
+        .attr("stroke", d => d3.rgb(d.color).darker(1))
+        .attr("fill", d => d.color)
+        .attr("opacity", 1e-6)
+        .transition()
+        .duration(duration / 2)
+        .attr("opacity", 0.67);
+    }
 
-      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();
+    // if the mouse is hovering over a chord, update it's tooltip
+    chordPaths.each(d => {
+      if (
+        this.popoverChord &&
+        this.popoverChord.source.orgindex === d.source.orgindex &&
+        this.popoverChord.source.orgsubindex === d.source.orgsubindex
+      ) {
+        //let popoverContent = this.chordTitle(d, matrix);
+        //this.displayTooltip(d3.event, popoverContent);
       }
+    });
 
-      // 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);
+    // attach mouse event handlers to the chords
+    chordPaths
+      .on("mouseover", this.mouseoverChord)
+      .on("mousemove", d => {
+        this.popoverChord = d;
+        let popoverContent = this.chordTitle(d, matrix);
+        this.displayTooltip(d3.event, popoverContent);
+      })
+      .on("mouseout", () => {
+        this.popoverChord = null;
+        this.setState({ showPopup: false });
+      });
 
-    // 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) );
-      };
+    let exitingChords = chordPaths.exit().attr("class", "exiting-chord");
+    exitingChords.remove();
+    /*
+    if (!this.switchedByAddress) {
+      // shrink chords to their center point upon removal
+      exitingChords
+        .transition()
+        .duration(duration / 2)
+        .attrTween("d", this.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();
     }
-    // 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) );
-      };
+*/
+    // keep track of this layout so we can animate from this layout to the next layout
+    this.last_chord = rechord;
+    this.last_labels = this.last_chord.arcData;
+    this.switchedByAddress = false;
+  };
+
+  displayTooltip = (event, content) => {
+    if (this.popupCancelled) {
+      this.setState({ showPopup: false });
+      return;
     }
+    let width = this.chordRef.offsetWidth;
+    let top = this.chordRef.offsetTop - 5;
+
+    // position the popup
+    let pwidth = this.popupRef.offsetWidth;
+    d3.select("#popover-div")
+      .style("left", Math.min(width - pwidth, event.pageX + 5) + "px")
+      .style("top", event.pageY - top + "px");
+    // show popup
+    this.setState({ showPopup: true, popupContent: content });
+  };
+
+  // animate the disappearance of an arc by shrinking it to its center point
+  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 t => {
+      return this.arcReference(tween(t));
+    };
+  };
+  // animate the exit of a chord by shrinking it to the center points of its arcs
+  chordTweenExit = d => {
+    let angle = d => (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);
 
-    // 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 t => {
+      return this.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
+  arcTween = oldLayout => {
+    var oldGroups = {};
+    if (oldLayout) {
+      oldLayout.arcData.forEach(groupData => {
+        oldGroups[groupData.index] = groupData;
+      });
+    }
+    return 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 (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) );
-        };
+
+      return t => {
+        return this.arcReference(tween(t));
       };
+    };
+  };
+
+  // animate all the chords to their new positions
+  tweenChordEnds = (chords, duration, last_layout) => {
+    let oldChords = {};
+    if (last_layout) {
+      last_layout.chordData.forEach(d => {
+        oldChords[d.key] = d;
+      });
     }
-
-    // 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}
-              };
+    let self = this;
+    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", () => {
+          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;
             }
-            interpolate = d3.interpolate(old, d);
-            return function(t) {
-              chord.attr('d', chordReference(interpolate(t)));
+          } 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 t => {
+            chord.attr("d", self.chordReference(interpolate(t)));
+          };
+        });
+    });
+  };
+
+  // animate a chord to its new color
+  tweenChordColor = (chords, duration, last_layout, style) => {
+    let oldChords = {};
+    if (last_layout) {
+      last_layout.chordData.forEach(d => {
+        oldChords[d.key] = d;
       });
     }
-
-    // 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 t => {
+            chord.style(style, interpolate(t));
+          };
         });
-      }
-      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
+  tickTween = oldArcs => {
+    var oldTicks = {};
+    if (oldArcs) {
+      oldArcs.forEach(d => {
+        oldTicks[d.key] = d;
       });
     }
-
-    // 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 = d => (d.startAngle + d.endAngle) / 2;
+    return d => {
+      var tween;
+      var old = oldTicks[d.key];
+      let start = angle(d);
+      let startTranslate = this.textRadius - 40;
+      let orient = d.angle > Math.PI ? "rotate(180)" : "";
+      if (old) {
+        //there's a matching old group
+        start = angle(old);
+        startTranslate = this.textRadius;
       }
-      let angle = function (d) {
-        return (d.startAngle + d.endAngle) / 2;
+      tween = d3.interpolateNumber(start, angle(d));
+      let same = start === angle(d);
+      let tsame = startTranslate === this.textRadius;
+
+      let transTween = d3.interpolateNumber(
+        startTranslate,
+        this.textRadius + 10
+      );
+
+      return t => {
+        let rot = same ? start : tween(t);
+        if (isNaN(rot)) rot = 0;
+        let tra = tsame ? this.textRadius + 10 : transTween(t);
+        return (
+          "rotate(" +
+          ((rot * 180) / Math.PI - 90) +
+          ") " +
+          "translate(" +
+          tra +
+          ",0)" +
+          orient
+        );
       };
-      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
+  mouseoverArc = d => {
+    d3.selectAll("path.chord").classed(
+      "fade",
+      p => d.index !== p.source.index && d.index !== p.target.index
+    );
+  };
+
+  // fade all chords except the given one
+  mouseoverChord = d => {
+    this.svg
+      .selectAll("path.chord")
+      .classed(
+        "fade",
+        p =>
+          !(
+            p.source.orgindex === d.source.orgindex &&
+            p.target.orgindex === d.target.orgindex
+          )
+      );
+  };
+
+  showAllChords = () => {
+    this.svg.selectAll("path.chord").classed("fade", false);
+  };
+
+  // called periodically to refresh the data
+  doUpdate = () => {
+    this.chordData.getMatrix().then(this.renderChord, e => {
+      console.log(ERROR_RENDERING + e);
+    });
+  };
 
-    // 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;
-      });
-    }
+  updateNow = () => {
+    clearInterval(this.interval);
+    this.chordData.getMatrix().then(this.renderChord, function(e) {
+      console.log(ERROR_RENDERING + e);
+    });
+    this.interval = setInterval(this.doUpdate, TRANSITION_DURATION);
+  };
+
+  // one of the legend sections was opened or closed
+  handleOpenChange = (id, isOpen) => {
+    console.log(`handleOpenChange called with ${id} and ${isOpen}`);
+    const { legendOptions } = this.state;
+    legendOptions[`${id}Open`] = isOpen;
+    this.setState({ legendOptions });
+  };
+
+  handleChangeOption = (checked, e) => {
+    const { legendOptions } = this.state;
+    const name = e.target.name;
+    legendOptions[name] = checked;
+    this.setState({ legendOptions }, () => {
+      if (name === "isRate") {
+        this.chordData.setRate(this.state.legendOptions.isRate);
 
-    // 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);
-      });
-    }
+        let doughnut = d3.select(DOUGHNUT);
+        if (!doughnut.empty()) {
+          this.fadeDoughnut();
+        }
+      } else {
+        d3.select("#legend").classed("byAddress", checked);
+        this.chordData.setConverter(
+          this.state.legendOptions.byAddress
+            ? separateAddresses
+            : aggregateAddresses
+        );
+        this.switchedByAddress = true;
+      }
+      this.updateNow();
+      localStorage[CHORDOPTIONSKEY] = JSON.stringify(this.state.legendOptions);
+    });
+  };
+
+  // one of the address checkboxes in the legend was clicked
+  handleChangeAddress = (address, checked) => {
+    const { addresses } = this.state;
+    addresses[address] = checked;
+    this.setState({ addresses }, () => {
+      this.fadeDoughnut();
+
+      this.excludedAddresses = [];
+      for (let address in this.state.addresses) {
+        if (!this.state.addresses[address])
+          this.excludedAddresses.push(address);
+      }
+      localStorage[CHORDFILTERKEY] = JSON.stringify(this.excludedAddresses);
+      if (this.chordData) this.chordData.setFilter(this.excludedAddresses);
+      this.updateNow();
+    });
+  };
 
-    function showAllChords() {
-      svg.selectAll('path.chord').classed('fade', false);
+  handleHoverAddress = (address, over) => {
+    if (over) {
+      this.enterLegend(address);
+    } else {
+      this.leaveLegend();
     }
+  };
 
-    // 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);
+  handleHoverRouter = (router, over) => {
+    if (over) {
+      this.enterRouter(router);
+    } else {
+      this.leaveLegend();
+    }
+  };
+
+  // called when mouse enters one of the address legends
+  enterLegend = addr => {
+    if (!this.state.legendOptions.byAddress) return;
+    // fade all chords that don't have this address
+    let indexes = [];
+    this.chordData.last_matrix.rows.forEach((row, r) => {
+      let addresses = this.chordData.last_matrix.getAddresses(r);
+      if (addresses.indexOf(addr) >= 0) indexes.push(r);
     });
-
-    // get the raw data and render the svg
-    chordData.getMatrix().then(function (matrix) {
-      // now that we have the routers and addresses, move the control switches and legend
-      $timeout(windowResized);
-      render(matrix);
-    }, function (e) {
-      console.log(ERROR_RENDERING + e);
+    d3.selectAll("path.chord").classed(
+      "fade",
+      p =>
+        indexes.indexOf(p.source.orgindex) < 0 &&
+        indexes.indexOf(p.target.orgindex) < 0
+    );
+  };
+
+  // called when mouse enters one of the router legends
+  enterRouter = router => {
+    let indexes = [];
+    // fade all chords that are not associated with this router
+    let agg = this.chordData.last_matrix.aggregate;
+    this.chordData.last_matrix.rows.forEach((row, r) => {
+      if (agg) {
+        if (row.chordName === router) indexes.push(r);
+      } else {
+        if (row.ingress === router || row.egress === router) indexes.push(r);
+      }
     });
-    // called periodically to refresh the data
-    function doUpdate() {
-      chordData.getMatrix().then(render, function (e) {
-        console.log(ERROR_RENDERING + e);
-      });
-    }
-    let interval = setInterval(doUpdate, transitionDuration);
-
+    d3.selectAll("path.chord").classed(
+      "fade",
+      p =>
+        indexes.indexOf(p.source.orgindex) < 0 &&
+        indexes.indexOf(p.target.orgindex) < 0
+    );
+  };
+  leaveLegend = () => {
+    this.showAllChords();
+  };
+
+  render() {
+    return (
+      <div ref={el => (this.chordRef = el)} className="qdrChord">
+        <div id="chord"></div>
+        <div
+          id="popover-div"
+          className={this.state.showPopup ? "qdrPopup" : "qdrPopup hidden"}
+          ref={el => (this.popupRef = el)}
+        >
+          <QDRPopup content={this.state.popupContent}></QDRPopup>
+        </div>
+        {this.state.showEmpty ? (
+          <div id="noTraffic">{this.state.emptyText}</div>
+        ) : (
+          <React.Fragment />
+        )}
+        <LegendComponent
+          handleOpenChange={this.handleOpenChange}
+          handleChangeOption={this.handleChangeOption}
+          handleChangeAddress={this.handleChangeAddress}
+          handleHoverAddress={this.handleHoverAddress}
+          handleHoverRouter={this.handleHoverRouter}
+          optionsOpen={this.state.legendOptions.optionsOpen}
+          routersOpen={this.state.legendOptions.routersOpen}
+          addressesOpen={this.state.legendOptions.addressesOpen}
+          isRate={this.state.legendOptions.isRate}
+          byAddress={this.state.legendOptions.byAddress}
+          arcColors={this.arcColors}
+          chordColors={this.chordColors}
+          addresses={this.state.addresses}
+        />
+      </div>
+    );
   }
 }
-ChordController.$inject = ['QDRService', '$scope', '$location', '$timeout', '$sce'];
+
+export default MessageFlowPage;
diff --git a/console/react/src/chord/ribbon/ribbon.js b/console/react/src/chord/ribbon/ribbon.js
index fb6996f..66c129d 100644
--- a/console/react/src/chord/ribbon/ribbon.js
+++ b/console/react/src/chord/ribbon/ribbon.js
@@ -16,30 +16,48 @@ KIND, either express or implied.  See the License for the
 specific language governing permissions and limitations
 under the License.
 */
-/* global d3 */
+
+import * as d3 from "d3";
+import * as d3path from "d3-path";
 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. 
+// 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
+// [0, 0] to 1/2 way to the center of the bezier curve.
+const dom = [0.06, 0.98, Math.PI];
+const ys = d3.scale
+  .linear()
+  .domain(dom)
+  .range([0.18, 0, 0]);
+const x0s = d3.scale
+  .linear()
+  .domain(dom)
+  .range([0.03, 0.24, 0.24]);
+const x1s = d3.scale
+  .linear()
+  .domain(dom)
+  .range([0.24, 0.6, 0.6]);
+const x2s = d3.scale
+  .linear()
+  .domain(dom)
+  .range([1.32, 0.8, 0.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 
+  // 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) {
+  var ribbon = function(d) {
     let sa0 = d.source.startAngle - halfPI,
       sa1 = d.source.endAngle - halfPI,
       ta0 = d.target.startAngle - halfPI,
@@ -68,34 +86,33 @@ function qdrRibbon() { // eslint-disable-line no-unused-vars
       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)) {
+      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];
+        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];
+        cp2 = [(ratiocp * (t1x + s0x)) / 2, (ratiocp * (t1y + s0y)) / 2];
       }
     }
 
     // construct the path using the control points
-    let path = d3.path();
+    let path = d3path.path();
     path.moveTo(s0x, s0y);
     path.arc(0, 0, r, sa0, sa1);
-    if (sa0 != ta0 || sa1 !== ta1) {
+    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 + '';
-
+    return path + "";
   };
-  ribbon.radius = function (radius) {
+  ribbon.radius = function(radius) {
     if (!arguments.length) return r;
     r = radius;
     return ribbon;
@@ -103,15 +120,19 @@ function qdrRibbon() { // eslint-disable-line no-unused-vars
   return ribbon;
 }
 
-let sqr = function (n) { return n * n; };
-let dist = function (p1x, p1y, p2x, p2y) { return sqr(p1x - p2x) + sqr(p1y - p2y);};
+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 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;
+  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)));
+  return Math.sqrt(dist(px, py, vx + t * (wx - vx), vy + t * (wy - vy)));
 };
 
 // See if x, y is contained in trapezoid.
@@ -120,31 +141,26 @@ let distToLine = function (vx, vy, wx, wy, px, py) {
 // 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 cpRatio = function(gap, x, y) {
   let top = ys(gap);
-  if (y >= top)
-    return 0;
+  if (y >= top) return 0;
 
   // get the xpoints of the trapezoid
   let x0 = x0s(gap);
-  if (x <= x0)
-    return 0;
+  if (x <= x0) return 0;
   let x3 = x3s(gap);
-  if (x > x3)
-    return 0;
+  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;
+  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;
+  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;
@@ -158,8 +174,11 @@ let cpRatio = function (gap, x, y) {
     // 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]);
+  let distScale = d3.scale
+    .linear()
+    .domain([0, top / 8, top / 2, top])
+    .range([0, 0.3, 0.4, 0.5]);
   return distScale(dist);
 };
 
-export { qdrRibbon };
\ No newline at end of file
+export { qdrRibbon };
diff --git a/console/react/src/chord/routersComponent.js b/console/react/src/chord/routersComponent.js
new file mode 100644
index 0000000..7b091fb
--- /dev/null
+++ b/console/react/src/chord/routersComponent.js
@@ -0,0 +1,56 @@
+/*
+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.
+*/
+
+import React, { Component } from "react";
+
+class RoutersComponent extends Component {
+  constructor(props) {
+    super(props);
+    this.state = {};
+  }
+
+  render() {
+    return (
+      <React.Fragment>
+        <ul className="routers">
+          {Object.keys(this.props.arcColors).map((router, i) => {
+            return (
+              <li
+                key={`router-${i}`}
+                className="legend-line"
+                onMouseEnter={() => this.props.handleHoverRouter(router, true)}
+                onMouseLeave={() => this.props.handleHoverRouter(router, false)}
+              >
+                <span
+                  className="legend-color"
+                  style={{ backgroundColor: this.props.arcColors[router] }}
+                ></span>
+                <span className="legend-router legend-text" title={router}>
+                  {router}
+                </span>
+              </li>
+            );
+          })}
+        </ul>
+      </React.Fragment>
+    );
+  }
+}
+
+export default RoutersComponent;
diff --git a/console/react/src/layout.js b/console/react/src/layout.js
index 2b43e9a..a2ace30 100644
--- a/console/react/src/layout.js
+++ b/console/react/src/layout.js
@@ -31,6 +31,7 @@ import ConnectPage from "./connectPage";
 import OverviewChartsPage from "./overviewChartsPage";
 import OverviewTablePage from "./overviewTablePage";
 import TopologyPage from "./topology/qdrTopology";
+import MessageFlowPage from "./chord/qdrChord";
 import { QDRService } from "./qdrService";
 const avatarImg = require("./assets/img_avatar.svg");
 
@@ -349,7 +350,11 @@ class PageLayout extends React.Component {
           />
         );
       } else if (this.state.activeGroup === "visualizations") {
-        return <TopologyPage service={this.service} />;
+        if (this.state.activeItem === "topology") {
+          return <TopologyPage service={this.service} />;
+        } else {
+          return <MessageFlowPage service={this.service} />;
+        }
       }
       //console.log("using overview charts page");
       return <OverviewChartsPage />;
diff --git a/console/react/src/topology/legend.js b/console/react/src/topology/legend.js
index 2f62ea6..b054d9a 100644
--- a/console/react/src/topology/legend.js
+++ b/console/react/src/topology/legend.js
@@ -113,37 +113,40 @@ export class Legend {
     } else {
       lsvg = d3.select("#topo_svg_legend svg g").selectAll("g");
     }
-    const isNew = look => this.nodes.nodes.some(n => look.cmp(n));
     // add a node to legendNodes for each node type that is currently in the svg
     let legendNodes = new Nodes(this.log);
-    lookFor.forEach(function(node, i) {
-      // if we haven't already added a node of this cls to the nodes
-      if (isNew(node)) {
-        let lnode = legendNodes.addUsing(
-          node.title,
-          node.text,
-          node.role,
-          undefined,
-          0,
-          0,
-          i,
-          0,
-          false,
-          node.props ? node.props : {}
-        );
-        if (node.cdir) lnode.cdir = node.cdir;
+    this.nodes.nodes.forEach((n, i) => {
+      let node = lookFor.find(lf => lf.cmp(n));
+      if (node) {
+        if (!legendNodes.nodes.some(ln => ln.key === node.title)) {
+          let newNode = legendNodes.addUsing(
+            node.title,
+            node.text,
+            node.role,
+            undefined,
+            0,
+            0,
+            i,
+            0,
+            false,
+            node.props ? node.props : {}
+          );
+          if (node.cdir) {
+            newNode.cdir = node.cdir;
+          }
+        }
       }
-    }, this);
+    });
 
     // determine the y coordinate of the last existing node in the legend
     let cury = 0;
     lsvg.each(function(d) {
-      cury += Nodes.radius(d.nodeType) * 2 + 10;
+      cury += Nodes.radius(d.nodeType) * 2 + 5;
     });
 
     // associate the legendNodes with lsvg
     lsvg = lsvg.data(legendNodes.nodes, function(d) {
-      return d.uid();
+      return d.key;
     });
 
     // add any new nodes
@@ -152,7 +155,7 @@ export class Legend {
       .append("svg:g")
       .attr("transform", function(d) {
         let t = `translate(0, ${cury})`;
-        cury += Nodes.radius(d.nodeType) * 2 + 10;
+        cury += Nodes.radius(d.nodeType) * 2 + 5;
         return t;
       });
     appendCircle(legendEnter, this.urlPrefix);
@@ -170,6 +173,10 @@ export class Legend {
     // remove any nodes that dropped out of legendNodes
     lsvg.exit().remove();
 
+    let svgEl = document.getElementById("svglegend");
+    if (svgEl) {
+      svgEl.style.height = `${cury + 20}px`;
+    }
     /*
     // position the legend based on it's size
     let svgEl = document.getElementById("svglegend");
diff --git a/console/react/src/topology/legendComponent.js b/console/react/src/topology/legendComponent.js
index 20795d3..2addba0 100644
--- a/console/react/src/topology/legendComponent.js
+++ b/console/react/src/topology/legendComponent.js
@@ -27,6 +27,7 @@ import {
 
 import ArrowsComponent from "./arrowsComponent";
 import TrafficComponent from "./trafficComponent";
+import MapLegendComponent from "./mapLegendComponent";
 
 class LegendComponent extends Component {
   constructor(props) {
@@ -99,7 +100,12 @@ class LegendComponent extends Component {
             isHidden={!this.props.mapOpen}
             isFixed
           >
-            <p>Map options go here</p>
+            <MapLegendComponent
+              open={this.props.mapOpen}
+              areaColor={this.props.areaColor}
+              oceanColor={this.props.oceanColor}
+              handleUpdateMapColor={this.props.handleUpdateMapColor}
+            />
           </AccordionContent>
         </AccordionItem>
         <AccordionItem>
diff --git a/console/react/src/topology/map.js b/console/react/src/topology/map.js
index 91942ed..6f9cc6f 100644
--- a/console/react/src/topology/map.js
+++ b/console/react/src/topology/map.js
@@ -46,133 +46,119 @@ export class BackgroundMap {
       scale: null
     };
   }
+  updateMapColor(which, color) {
+    console.log(`map received request to update ${which} color to ${color}`);
+    if (which === "areaColor") {
+      this.updateLandColor(color);
+    } else if (which === "oceanColor") {
+      this.updateOceanColor(color);
+    }
+    return this.mapOptions;
+  }
   updateLandColor(color) {
-    localStorage[MAPOPTIONSKEY] = JSON.stringify(this.$scope.mapOptions);
+    this.mapOptions.areaColor = color;
+    localStorage[MAPOPTIONSKEY] = JSON.stringify(this.mapOptions);
     d3.select("g.geo path.land")
       .style("fill", color)
       .style("stroke", d3.rgb(color).darker());
   }
   updateOceanColor(color) {
-    localStorage[MAPOPTIONSKEY] = JSON.stringify(this.$scope.mapOptions);
-    if (!color) color = this.$scope.mapOptions.oceanColor;
+    this.mapOptions.oceanColor = color;
+    localStorage[MAPOPTIONSKEY] = JSON.stringify(this.mapOptions);
+    if (!color) color = this.mapOptions.oceanColor;
     d3.select("g.geo rect.ocean").style("fill", color);
-    if (this.$scope.legendOptions.map.open) {
-      d3.select("#main_container").style("background-color", color);
+    if (this.$scope.state.legendOptions.map.open) {
+      d3.select(".pf-c-page__main").style("background-color", color);
     } else {
-      d3.select("#main_container").style("background-color", "#FFF");
+      d3.select(".pf-c-page__main").style("background-color", "#FFF");
     }
   }
 
   init($scope, svg, width, height) {
-    return new Promise(
-      function(resolve, reject) {
-        if (this.initialized) {
-          resolve();
-          return;
-        }
-        this.svg = svg;
-        this.width = width;
-        this.height = height;
-        // track last translation and scale event we processed
-        this.rotate = 20;
-        this.scaleExtent = [1, 10];
-
-        // handle ui events to change the colors
-        $scope.$watch(
-          "mapOptions.areaColor",
-          function(newValue, oldValue) {
-            if (newValue !== oldValue) {
-              this.updateLandColor(newValue);
-            }
-          }.bind(this)
-        );
-        $scope.$watch(
-          "mapOptions.oceanColor",
-          function(newValue, oldValue) {
-            if (newValue !== oldValue) {
-              this.updateOceanColor(newValue);
-            }
-          }.bind(this)
-        );
-
-        // setup the projection with some defaults
-        this.projection = d3.geo
-          .mercator()
-          .rotate([this.rotate, 0])
-          .scale(1)
-          .translate([width / 2, height / 2]);
-
-        // this path will hold the land coordinates once they are loaded
-        this.geoPath = d3.geo.path().projection(this.projection);
-
-        // set up the scale extent and initial scale for the projection
-        var b = getMapBounds(this.projection, Math.max(maxnorth, maxsouth)),
-          s = width / (b[1][0] - b[0][0]);
-        this.scaleExtent = [s, 15 * s];
-
-        this.projection.scale(this.scaleExtent[0]);
-        this.lastProjection = angular.fromJson(
-          localStorage[MAPPOSITIONKEY]
-        ) || {
-          rotate: 20,
-          scale: this.scaleExtent[0],
-          translate: [width / 2, height / 2]
-        };
+    return new Promise((resolve, reject) => {
+      if (this.initialized) {
+        resolve();
+        return;
+      }
+      this.svg = svg;
+      this.width = width;
+      this.height = height;
+      // track last translation and scale event we processed
+      this.rotate = 20;
+      this.scaleExtent = [1, 10];
+
+      // setup the projection with some defaults
+      this.projection = d3.geo
+        .mercator()
+        .rotate([this.rotate, 0])
+        .scale(1)
+        .translate([width / 2, height / 2]);
+
+      // this path will hold the land coordinates once they are loaded
+      this.geoPath = d3.geo.path().projection(this.projection);
+
+      // set up the scale extent and initial scale for the projection
+      var b = getMapBounds(this.projection, Math.max(maxnorth, maxsouth)),
+        s = width / (b[1][0] - b[0][0]);
+      this.scaleExtent = [s, 15 * s];
+
+      this.projection.scale(this.scaleExtent[0]);
+
+      let savedOptions = localStorage.getItem(MAPPOSITIONKEY);
+      this.lastProjection = savedOptions
+        ? JSON.parse(savedOptions)
+        : {
+            rotate: 20,
+            scale: this.scaleExtent[0],
+            translate: [width / 2, height / 2]
+          };
+
+      this.zoom = d3.behavior
+        .zoom()
+        .scaleExtent(this.scaleExtent)
+        .scale(this.projection.scale())
+        .translate([0, 0]) // not linked directly to projection
+        .on("zoom", this.zoomed.bind(this));
+
+      this.geo = svg
+        .append("g")
+        .attr("class", "geo")
+        .style("opacity", this.$scope.state.legendOptions.map.open ? "1" : "0");
+
+      this.geo
+        .append("rect")
+        .attr("class", "ocean")
+        .attr("width", width)
+        .attr("height", height)
+        .attr("fill", "#FFF");
+
+      if (this.$scope.state.legendOptions.map.open) {
+        this.svg.call(this.zoom).on("dblclick.zoom", null);
+      }
 
-        this.zoom = d3.behavior
-          .zoom()
-          .scaleExtent(this.scaleExtent)
-          .scale(this.projection.scale())
-          .translate([0, 0]) // not linked directly to projection
-          .on("zoom", this.zoomed.bind(this));
+      // async load of data file. calls resolve when this completes to let caller know
+      d3.json("data/countries.json", (error, world) => {
+        if (error) reject(error);
+        this.geo
+          .append("path")
+          .datum(topojson.feature(world, world.objects.countries))
+          .attr("class", "land")
+          .attr("d", this.geoPath)
+          .style("stroke", d3.rgb(this.mapOptions.areaColor).darker());
 
-        this.geo = svg
-          .append("g")
-          .attr("class", "geo")
-          .style("opacity", this.$scope.legendOptions.map.open ? "1" : "0");
+        this.updateLandColor(this.mapOptions.areaColor);
+        this.updateOceanColor(this.mapOptions.oceanColor);
 
-        this.geo
-          .append("rect")
-          .attr("class", "ocean")
-          .attr("width", width)
-          .attr("height", height)
-          .attr("fill", "#FFF");
-
-        if (this.$scope.legendOptions.map.open) {
-          this.svg.call(this.zoom).on("dblclick.zoom", null);
-        }
-
-        // async load of data file. calls resolve when this completes to let caller know
-        d3.json(
-          "plugin/data/countries.json",
-          function(error, world) {
-            if (error) reject(error);
-
-            this.geo
-              .append("path")
-              .datum(topojson.feature(world, world.objects.countries))
-              .attr("class", "land")
-              .attr("d", this.geoPath)
-              .style(
-                "stroke",
-                d3.rgb(this.$scope.mapOptions.areaColor).darker()
-              );
-
-            this.updateLandColor(this.$scope.mapOptions.areaColor);
-            this.updateOceanColor(this.$scope.mapOptions.oceanColor);
-
-            // restore map rotate, scale, translate
-            this.restoreState();
-
-            // draw with current positions
-            this.geo.selectAll(".land").attr("d", this.geoPath);
-
-            this.initialized = true;
-            resolve();
-          }.bind(this)
-        );
-      }.bind(this)
-    );
+        // restore map rotate, scale, translate
+        this.restoreState();
+
+        // draw with current positions
+        this.geo.selectAll(".land").attr("d", this.geoPath);
+
+        this.initialized = true;
+        resolve();
+      });
+    });
   }
 
   setMapOpacity(opacity) {
@@ -212,7 +198,7 @@ export class BackgroundMap {
       d3.event &&
       !this.$scope.current_node &&
       !this.$scope.mousedown_node &&
-      this.$scope.legendOptions.map.open
+      this.$scope.state.legendOptions.map.open
     ) {
       let scale = d3.event.scale,
         t = d3.event.translate,
@@ -223,8 +209,8 @@ export class BackgroundMap {
       // zoomed
       if (scale !== this.last.scale) {
         // get the mouse's x,y relative to the svg
-        let top = d3.select("#main_container").node().offsetTop;
-        let left = d3.select("#main_container").node().offsetLeft;
+        let top = d3.select(".pf-c-page__main").node().offsetTop;
+        let left = d3.select(".pf-c-page__main").node().offsetLeft;
         let mx = d3.event.sourceEvent.clientX - left;
         let my = d3.event.sourceEvent.clientY - top - 1;
 
diff --git a/console/react/src/topology/mapLegendComponent.jsx b/console/react/src/topology/mapLegendComponent.jsx
new file mode 100644
index 0000000..e68f9b9
--- /dev/null
+++ b/console/react/src/topology/mapLegendComponent.jsx
@@ -0,0 +1,94 @@
+/*
+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.
+*/
+
+import React, { Component } from "react";
+
+class MapLegendComponent extends Component {
+  constructor(props) {
+    super(props);
+    this.state = {};
+  }
+
+  dotClicked = address => {
+    this.props.handleChangeTrafficFlowAddress(
+      address,
+      !this.props.addresses[address]
+    );
+  };
+  coloredDot = (address, i) => {
+    return (
+      <svg
+        className="address-svg"
+        id={`address-dot-${i}`}
+        width="200"
+        height="20"
+      >
+        <g
+          transform="translate(10,10)"
+          onClick={() => this.dotClicked(address)}
+        >
+          <circle r="10" fill={this.props.addressColors[address]} />
+          {this.props.addresses[address] ? (
+            <text x="-8" y="5" className="address-checkbox">
+              &#xf00c;
+            </text>
+          ) : (
+            ""
+          )}
+          <text x="20" y="5" className="label">
+            {address}
+          </text>
+        </g>
+      </svg>
+    );
+  };
+
+  handleColorChange = e => {
+    this.props.handleUpdateMapColor(e.target.name, e.target.value);
+  };
+
+  render() {
+    return (
+      <ul className="map-legend">
+        <li>
+          <input
+            id="areaColor"
+            name="areaColor"
+            type="color"
+            value={this.props.areaColor}
+            onChange={this.handleColorChange}
+          />{" "}
+          <label htmlFor="areaColor">Land</label>
+        </li>
+        <li>
+          <input
+            id="oceanColor"
+            name="oceanColor"
+            type="color"
+            value={this.props.oceanColor}
+            onChange={this.handleColorChange}
+          />{" "}
+          <label htmlFor="oceanColor">Ocean</label>
+        </li>
+      </ul>
+    );
+  }
+}
+
+export default MapLegendComponent;
diff --git a/console/react/src/topology/qdrTopology.js b/console/react/src/topology/qdrTopology.js
index 15b1b3a..7a2d1ec 100644
--- a/console/react/src/topology/qdrTopology.js
+++ b/console/react/src/topology/qdrTopology.js
@@ -68,6 +68,7 @@ class TopologyPage extends Component {
             clientArrows: true
           }
         };
+
     this.state = {
       popupContent: "",
       showPopup: false,
@@ -94,12 +95,11 @@ class TopologyPage extends Component {
       this.forceData,
       ["dots", "congestion"].filter(t => this.state.legendOptions.traffic[t])
     );
-    this.backgroundMap = new BackgroundMap(this, () => {});
     this.backgroundMap = new BackgroundMap(
       this,
       // notify: called each time a pan/zoom is performed
       () => {
-        if (this.legendOptions.map.open) {
+        if (this.state.legendOptions.map.open) {
           // set all the nodes' x,y position based on their saved lon,lat
           this.forceData.nodes.setXY(this.backgroundMap);
           this.forceData.nodes.savePositions();
@@ -109,6 +109,7 @@ class TopologyPage extends Component {
         }
       }
     );
+    this.state.mapOptions = this.backgroundMap.mapOptions;
   }
 
   // called only once when the component is initialized
@@ -153,7 +154,7 @@ class TopologyPage extends Component {
         .init(this, this.svg, this.width, this.height)
         .then(() => {
           this.forceData.nodes.saveLonLat(this.backgroundMap);
-          this.backgroundMap.setMapOpacity(this.legendOptions.map.open);
+          this.backgroundMap.setMapOpacity(this.state.legendOptions.map.open);
         });
       addDefs(this.svg);
       addGradient(this.svg);
@@ -575,7 +576,7 @@ class TopologyPage extends Component {
       })
       .on("mouseup", function(d) {
         // mouse up for circle
-        this.backgroundMap.restartZoom();
+        self.backgroundMap.restartZoom();
         if (!self.mousedown_node) return;
 
         // unenlarge target node
@@ -862,6 +863,11 @@ class TopologyPage extends Component {
       this.handleLegendOptionsChange(legendOptions);
     }
   };
+  handleUpdateMapColor = (which, color) => {
+    let mapOptions = this.backgroundMap.updateMapColor(which, color);
+    this.setState({ mapOptions });
+  };
+
   render() {
     return (
       <div className="qdrTopology">
@@ -876,10 +882,13 @@ class TopologyPage extends Component {
           congestion={this.state.legendOptions.traffic.congestion}
           routerArrows={this.state.legendOptions.arrows.routerArrows}
           clientArrows={this.state.legendOptions.arrows.clientArrows}
+          areaColor={this.state.mapOptions.areaColor}
+          oceanColor={this.state.mapOptions.oceanColor}
           handleOpenChange={this.handleOpenChange}
           handleChangeArrows={this.handleChangeArrows}
           handleChangeTrafficAnimation={this.handleChangeTrafficAnimation}
           handleChangeTrafficFlowAddress={this.handleChangeTrafficFlowAddress}
+          handleUpdateMapColor={this.handleUpdateMapColor}
         />
 
         <div className="diagram">
diff --git a/console/react/src/topology/traffic.js b/console/react/src/topology/traffic.js
index 9b95322..e43d137 100644
--- a/console/react/src/topology/traffic.js
+++ b/console/react/src/topology/traffic.js
@@ -24,7 +24,7 @@ import { nextHop } from "./topoUtils.js";
 import { utils } from "../amqp/utilities.js";
 
 const transitionDuration = 1000;
-const CHORDFILTERKEY = "chordFilter";
+//const CHORDFILTERKEY = "chordFilter";
 
 export class Traffic {
   // eslint-disable-line no-unused-vars
@@ -611,7 +611,8 @@ class Dots extends TrafficAnimation {
       // Now find the created node that each link is associated with
       for (let linkIndex = 0; linkIndex < foundLinks.length; linkIndex++) {
         // use .some so the loop stops at the 1st match
-        nodes.some(function(node) {
+        // eslint-disable-next-line no-loop-func
+        nodes.some(node => {
           if (
             node.normals &&
             node.normals.some(function(normal) {


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