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">
+ 
+ </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">
+ 
+ </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