console/react/src/chord/chordViewer.js (669 lines of code) (raw):
/*
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 { TopologyView, TopologySideBar } from "@patternfly/react-topology";
import { getSizes } from "../topology/topoUtils";
import { separateAddresses, aggregateAddresses } from "./filters.js";
import { ChordData } from "./data.js";
import { qdrRibbon } from "./ribbon/ribbon.js";
import { qdrlayoutChord } from "./layout/layout.js";
import ChordToolbar from "./chordToolbar";
import QDRPopup from "../common/qdrPopup";
import * as d3 from "d3";
import PropTypes from "prop-types";
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 ChordViewer extends Component {
static propTypes = {
service: PropTypes.object.isRequired
};
constructor(props) {
super(props);
this.state = {
addresses: {},
showPopup: false,
popupContent: "",
showEmpty: false,
emptyText: ""
};
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
};
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
this.formatNumber = d3.format(",.1f");
// colors
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++) {
this.colorGen(i);
}
// keep track of previous chords so we can animate to the new values
this.last_chord = null;
this.last_labels = null;
// global pointer to the diagram
this.svg = null;
this.popoverChord = null;
this.popoverArc = null;
this.theyveBeenWarned = false;
this.arcColors = {};
}
// called only once when the component is initialized
componentDidMount() {
this.init();
}
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);
}
init = () => {
this.setSizes();
// 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);
// 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)
.attr("aria-label", "chord-svg")
.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();
});
this.svg = this.svg.append("g").attr("class", "chart-container");
window.addEventListener("resize", () => {
this.windowResized();
setTimeout(this.windowResized, 1);
});
// 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 = () => {
const { width, height } = getSizes("chordContainer");
return Math.max(Math.floor((Math.min(width, height) * 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;
}
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();
}
// 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 = this.decorateArcData(rechord.groups, matrix);
// join the decorated data with a d3 selection
let arcsGroup = this.svg.selectAll("g.arc").data(rechord.arcData, d => {
return d.key;
});
// get a d3 selection of all the new arcs that have been added
let newArcs = arcsGroup
.enter()
.append("svg:g")
.attr("class", "arc")
.attr("aria-label", d => d.key);
// 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 });
});
// 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 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;
};
});
exitingArcs
.selectAll("path")
.transition()
.duration(duration / 2)
.attrTween("d", this.arcTweenExit)
.each("end", function() {
d3.select(this)
.node()
.parentNode.remove();
});
// 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);
// new chords are paths
chordPaths
.enter()
.append("path")
.attr("class", "chord");
if (!this.switchedByAddress) {
// do multiple concurrent tweens on the chords
chordPaths
.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
.attr("d", d => {
return this.chordReference(d);
})
.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 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);
}
});
// 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 });
});
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();
}
*/
// 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;
}
d3.select("#popover-div")
.style("left", `${event.pageX + 5}px`)
.style("top", `${event.pageY}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);
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 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;
});
}
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;
}
} 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;
});
}
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));
};
});
});
};
// animate the arc labels to their new locations
tickTween = oldArcs => {
var oldTicks = {};
if (oldArcs) {
oldArcs.forEach(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;
}
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}`;
};
};
};
// 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);
});
};
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) => {
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);
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();
});
};
handleHoverAddress = (address, over) => {
if (over) {
this.enterLegend(address);
} else {
this.leaveLegend();
}
};
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);
});
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);
}
});
d3.selectAll("path.chord").classed(
"fade",
p =>
indexes.indexOf(p.source.orgindex) < 0 && indexes.indexOf(p.target.orgindex) < 0
);
};
leaveLegend = () => {
this.showAllChords();
};
render() {
return (
<TopologyView
aria-label="chord-viewer"
viewToolbar={
<ChordToolbar
handleOpenChange={this.handleOpenChange}
handleChangeOption={this.handleChangeOption}
handleChangeAddress={this.handleChangeAddress}
handleHoverAddress={this.handleHoverAddress}
handleHoverRouter={this.handleHoverRouter}
isRate={this.state.legendOptions.isRate}
byAddress={this.state.legendOptions.byAddress}
arcColors={this.arcColors}
chordColors={this.chordColors}
addresses={this.state.addresses}
/>
}
sideBar={<TopologySideBar show={false}></TopologySideBar>}
sideBarOpen={false}
className="qdrTopology"
>
<div id="chordContainer" className="qdrChord">
{this.state.showEmpty ? (
<div aria-label="chord-no-traffic" id="noTraffic">
{this.state.emptyText}
</div>
) : (
<div aria-label="chord-traffic" />
)}
<div aria-label="chord-diagram" 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>
</div>
</TopologyView>
);
}
}
export default ChordViewer;