This is an automated email from the ASF dual-hosted git repository. eallen pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/qpid-dispatch.git
The following commit(s) were added to refs/heads/master by this push: new 9a6eda8 DISPATCH-1474 Ensure all inter-router links are included in the console's traffic view 9a6eda8 is described below commit 9a6eda8fd2103afc7af225f08b5d81643c56c63e Author: Ernest Allen <eal...@redhat.com> AuthorDate: Wed Dec 11 17:36:31 2019 -0500 DISPATCH-1474 Ensure all inter-router links are included in the console's traffic view --- console/react/src/topology/traffic.js | 631 ++++++++++++++++++++-------------- 1 file changed, 379 insertions(+), 252 deletions(-) diff --git a/console/react/src/topology/traffic.js b/console/react/src/topology/traffic.js index 4b4caa8..14fc172 100644 --- a/console/react/src/topology/traffic.js +++ b/console/react/src/topology/traffic.js @@ -24,18 +24,16 @@ import { nextHop } from "./topoUtils.js"; import { utils } from "../common/amqp/utilities.js"; const transitionDuration = 1000; -//const CHORDFILTERKEY = "chordFilter"; export class Traffic { // eslint-disable-line no-unused-vars - constructor($scope, QDRService, converter, radius, topology, types) { + constructor($scope, QDRService, converter, radius, topology, types, addressesChanged) { this.QDRService = QDRService; + this.addressesChanged = addressesChanged; this.types = []; this.viss = []; this.topology = topology; // contains the list of router nodes this.$scope = $scope; - this.addresses = $scope.state.legendOptions.traffic.addresses; - this.addressColors = $scope.state.legendOptions.traffic.addressColors; // internal variables this.interval = null; // setInterval handle types.forEach( @@ -43,7 +41,7 @@ export class Traffic { this.addAnimationType(t, converter, radius); }.bind(this) ); - // called by angular when mouse enters one of the address legends + // called when mouse enters one of the address legends this.$scope.enterLegend = address => { // fade all flows that aren't for this address this.fadeOtherAddresses(address); @@ -72,6 +70,7 @@ export class Traffic { // start updating the traffic data start() { this.stop(); + this.setup(); this.doUpdate(); this.interval = setInterval(this.doUpdate.bind(this), transitionDuration); } @@ -99,10 +98,44 @@ export class Traffic { } this.start(); } + // set the data needed for each animation + setup() { + this.viss.forEach(v => v.setup()); + } // called periodically to refresh the traffic flow doUpdate() { + if (!this.interval) return; this.viss.forEach(v => v.doUpdate()); } + + setTopology(topology) { + this.topology = topology; + } + + getAddressColors(service, converter) { + return Dots.getAddressColors(service, converter); + } + + updateAddressColors(address, checked) { + if (addressColors[address]) { + addressColors[address].checked = checked; + } + this.updateDots(); + } + + updateDots() { + const dots = this.viss.find(v => v.type === "dots"); + if (dots) { + const excludedAddresses = Object.keys(addressColors).filter( + address => !addressColors[address].checked + ); + dots.updateAddresses(excludedAddresses); + } + } + + addressColors() { + return addressColors; + } } /* Base class for congestion and dots visualizations */ @@ -129,6 +162,7 @@ class TrafficAnimation { } } +const STEPS = 6; /* Color the links between router to show how heavily used the links are. */ class Congestion extends TrafficAnimation { constructor(traffic) { @@ -136,6 +170,18 @@ class Congestion extends TrafficAnimation { this.type = "congestion"; this.stopped = false; } + + setup() { + this.traffic.QDRService.management.topology.addUpdateEntities([ + { + entity: "router.link" + }, + { + entity: "connection" + } + ]); + } + findResult(node, entity, attribute, value) { let attrIndex = node[entity].attributeNames.indexOf(attribute); if (attrIndex >= 0) { @@ -147,105 +193,106 @@ class Congestion extends TrafficAnimation { } return null; } + doUpdate() { this.stopped = false; let self = this; - this.traffic.QDRService.management.topology.ensureAllEntities( - [ - { - entity: "router.link", - force: true - }, - { - entity: "connection" - } - ], - function() { - // animation was stopped between the ensureAllEntities request and the response - if (self.stopped) return; - let links = {}; - let nodeInfo = self.traffic.QDRService.management.topology.nodeInfo(); - const nodes = self.traffic.topology.nodes.nodes; - const srv = self.traffic.QDRService; - // accumulate all the inter-router links in an object - // keyed by the svgs path id - for (let nodeId in nodeInfo) { - let node = nodeInfo[nodeId]; - let nodeLinks = node["router.link"]; - if (!nodeLinks) continue; - for (let n = 0; n < nodeLinks.results.length; n++) { - let link = srv.utilities.flatten( - nodeLinks.attributeNames, - nodeLinks.results[n] - ); - if (link.linkType !== "router-control") { - let f = self.nodeIndexFor(nodes, srv.utilities.nameFromId(nodeId)); - let connection = self.findResult( - node, - "connection", - "identity", - link.connectionId + // animation was stopped between the ensureAllEntities request and the response + if (self.stopped) return; + let links = {}; + let nodeInfo = self.traffic.QDRService.management.topology.nodeInfo(); + const nodes = self.traffic.topology.nodes.nodes; + const srv = self.traffic.QDRService; + // accumulate all the inter-router links in an object + // keyed by the svgs path id + for (let nodeId in nodeInfo) { + let node = nodeInfo[nodeId]; + let nodeLinks = node["router.link"]; + if (!nodeLinks) continue; + for (let n = 0; n < nodeLinks.results.length; n++) { + let link = srv.utilities.flatten(nodeLinks.attributeNames, nodeLinks.results[n]); + if (link.linkType !== "router-control") { + let f = self.nodeIndexFor(nodes, srv.utilities.nameFromId(nodeId)); + let connection = self.findResult( + node, + "connection", + "identity", + link.connectionId + ); + if (connection) { + let t = self.nodeIndexFor(nodes, connection.container); + let little = Math.min(f, t); + let big = Math.max(f, t); + if (little >= 0) { + let key = ["#hitpath", nodes[little].uid(srv), nodes[big].uid(srv)].join( + "-" ); - if (connection) { - let t = self.nodeIndexFor(nodes, connection.container); - let little = Math.min(f, t); - let big = Math.max(f, t); - if (little >= 0) { - let key = ["#path", nodes[little].uid(srv), nodes[big].uid(srv)].join( - "-" - ); - if (!links[key]) links[key] = []; - links[key].push(link); - } - } + if (!links[key]) links[key] = []; + links[key].push(link); } } } - // accumulate the colors/directions to be used - for (let key in links) { - let congestion = self.congestion(links[key]); - let pathId = key.replace(/\./g, "\\.").replace(/ /g, "\\ "); - let path = d3.select(pathId); - if (path && !path.empty()) { - path - .transition() - .duration(1000) - .style("stroke", congestion) - .each("end", function(d) { - if (self.stopped) { - d3.select(this).style("stroke", "black"); - } - }); - } + } + } + // accumulate the colors/directions to be used + for (let key in links) { + let congestion = self.congestion(links[key]); + let pathId = key.replace(/\./g, "\\.").replace(/ /g, "\\ "); + let path = d3.select(pathId); + if (path && !path.empty()) { + // start the path with transparent white + if (!path.attr("style")) { + path.style("stroke", "rgb(255, 255, 255)").style("opacity", 0); } + // transition to next color + path + .transition() + .duration(1000) + .style("stroke", congestion) + .style("opacity", 0.2) + .each("end", function(d) { + if (self.stopped) { + // fade to transparent + const p = d3.select(this); + p.transition() + .duration(100) + .style("opacity", 0) + .each("end", function() { + // remove the style + p.style("stroke", null).style("opacity", null); + }); + } + }); } - ); + } } congestion(links) { + const max = STEPS - 1; let v = 0; for (let l = 0; l < links.length; l++) { let link = links[l]; v = Math.max( v, - (3 * (link.undeliveredCount + link.unsettledCount)) / link.capacity + (max * (link.undeliveredCount + link.unsettledCount)) / link.capacity ); - if (link.deliveriesDelayed1Sec || link.deliveriesDelayed10Sec) v = 3; - v = Math.min(3, v); + v = Math.min(max, v); } return this.fillColor(v); } fillColor(v) { let color = d3.scale .linear() - .domain([0, 1, 2, 3]) + .domain([...Array(STEPS).keys()]) .interpolate(d3.interpolateHcl) .range([ d3.rgb("#999999"), d3.rgb("#00FF00"), + d3.rgb("#66CC00"), d3.rgb("#FFA500"), + d3.rgb("#FFCC00"), d3.rgb("#FF0000") ]); - return color(Math.max(0, Math.min(3, v))); + return color(Math.max(0, Math.min(STEPS - 1, v))); } remove() { this.stopped = true; @@ -253,15 +300,15 @@ class Congestion extends TrafficAnimation { .selectAll("path.traffic") .classed("traffic", false); d3.select("#SVG_ID") - .selectAll("path.link") - .style("stroke", "black"); - d3.select("#SVG_ID") .select("defs.custom-markers") .selectAll("marker") .remove(); } } +const colorGen = d3.scale.category10(); +const addressColors = {}; + /* Create animated dots moving along the links between routers to show message flow */ class Dots extends TrafficAnimation { @@ -272,24 +319,34 @@ class Dots extends TrafficAnimation { this.lastFlows = {}; // the number of dots animated between routers this.stopped = false; this.chordData = new ChordData(this.traffic.QDRService, true, converter); // gets ingressHistogram data - // colors - this.colorGen = d3.scale.category10(); - for (let i = 0; i < 10; i++) { - this.colorGen(i); + if (Object.keys(addressColors).length === 0) { + // map address to {color, checked} + Dots.getAddressColors(this.traffic.QDRService, converter); } - this.chordData.getMatrix().then(() => { - const addresses = this.chordData.getAddresses(); - const addressColors = {}; - for (let address in addresses) { - this.fillColor(address, addressColors); - } - traffic.updateAddresses = this.updateAddresses; - this.traffic.$scope.handleUpdatedAddresses(addresses); - this.traffic.$scope.handleUpdateAddressColors(addressColors); - // set excludedAddresses - this.updateAddresses(); + } + + static getAddressColors(service, converter) { + return new Promise(resolve => { + const chordData = new ChordData(service, true, converter); + chordData.getMatrix().then(() => { + const addresses = chordData.getAddresses(); + for (let address in addresses) { + addressColors[address] = { color: colorGen(address), checked: true }; + } + resolve(addressColors); + }); }); } + + setup() { + this.traffic.QDRService.management.topology.addUpdateEntities([ + { + entity: "router.node", + attrs: ["id", "nextHop"] + } + ]); + } + remove() { d3.select("#SVG_ID") .selectAll("path.traffic") @@ -300,15 +357,13 @@ class Dots extends TrafficAnimation { this.lastFlows = {}; this.stopped = true; } - updateAddresses() { - this.excludedAddresses = []; - for (const address in this.traffic.addresses) { - if (!this.traffic.addresses[address]) this.excludedAddresses.push(address); - } - if (this.chordData) { - this.chordData.setFilter(this.excludedAddresses); - } + + updateAddresses(excludedAddresses) { + this.chordData.setFilter(excludedAddresses); + // make sure no excludedAddress have an animation + this.doUpdate(); } + fadeOtherAddresses(address) { d3.selectAll("circle.flow").classed("fade", function(d) { return d.address !== address; @@ -317,173 +372,177 @@ class Dots extends TrafficAnimation { unFadeAll() { d3.selectAll("circle.flow").classed("fade", false); } + doUpdate() { - let self = this; this.stopped = false; - // we need the nextHop data to show traffic between routers that are connected by intermediaries - this.traffic.QDRService.management.topology.ensureAllEntities( - [ - { - entity: "router.node", - attrs: ["id", "nextHop"] - } - ], - function() { - // if we were stopped between the request and response, just exit - if (self.stopped) return; - // get the ingressHistogram data for all routers - self.chordData.getMatrix().then(self.render.bind(self), function(e) { - console.log("Could not get message histogram" + e); - }); - } - ); + // get the ingressHistogram data for all routers + this.chordData.getMatrix().then(this.render); } - render(matrix) { - if (this.stopped === false) { - const addresses = this.chordData.getAddresses(); - this.traffic.$scope.handleUpdatedAddresses(addresses); - const addressColors = {}; - for (let address in addresses) { - this.fillColor(address, addressColors); + + render = matrix => { + if (this.stopped) return; + const addresses = this.chordData.getAddresses(); + let changed = false; + // make sure each address has a color + for (let address in addresses) { + if (!(address in addressColors)) { + addressColors[address] = { color: this.fillColor(address), checked: true }; + changed = true; } - this.traffic.$scope.handleUpdateAddressColors(addressColors); - - // get the rate of message flow between routers - let hops = {}; // every hop between routers that is involved in message flow - let matrixMessages = matrix.matrixMessages(); - // the fastest traffic rate gets 3 times as many dots as the slowest - let minmax = matrix.getMinMax(); - let flowScale = d3.scale - .linear() - .domain(minmax) - .range([1, 1.1]); - // row is ingress router, col is egress router. Value at [row][col] is the rate - matrixMessages.forEach( - function(row, r) { - row.forEach( - function(val, c) { - if (val > MIN_CHORD_THRESHOLD) { - // translate between matrix row/col and node index - let f = this.nodeIndexFor( - this.traffic.topology.nodes.nodes, - matrix.rows[r].egress - ); - let t = this.nodeIndexFor( - this.traffic.topology.nodes.nodes, - matrix.rows[r].ingress - ); - let address = matrix.getAddress(r, c); - if (r !== c) { - // accumulate the hops between the ingress and egress routers - nextHop( - this.traffic.topology.nodes.nodes[f], - this.traffic.topology.nodes.nodes[t], - this.traffic.topology.nodes, - this.traffic.topology.links, - this.traffic.QDRService.management.topology.nodeInfo(), - this.traffic.topology.nodes.nodes[f], - function(link, fnode, tnode) { - let key = "-" + link.uid; - let back = fnode.index < tnode.index; - if (!hops[key]) hops[key] = []; - hops[key].push({ - val: val, - back: back, - address: address - }); - } - ); - } - // Find the senders connected to nodes[f] and the receivers connected to nodes[t] - // and add their links to the animation - this.addClients( - hops, - this.traffic.topology.nodes.nodes, - f, - val, - true, - address - ); - this.addClients( - hops, - this.traffic.topology.nodes.nodes, - t, - val, - false, - address - ); - } - }.bind(this) + } + // remove any address that no longer have traffic + for (let address in addressColors) { + if (!(address in addresses)) { + delete addressColors[address]; + changed = true; + } + } + if (changed) { + this.traffic.addressesChanged(); + } + + // get the rate of message flow between routers + let hops = {}; // every hop between routers that is involved in message flow + let matrixMessages = matrix.matrixMessages(); + // the fastest traffic rate gets more dots than the slowest + let minmax = matrix.getMinMax(); + let flowScale = d3.scale + .linear() + .domain(minmax) + .range([1, 1.5]); + // row is ingress router, col is egress router. Value at [row][col] is the rate + matrixMessages.forEach((row, r) => { + row.forEach((val, c) => { + if (val > MIN_CHORD_THRESHOLD) { + // translate between matrix row/col and node index + let f = this.nodeIndexFor( + this.traffic.topology.nodes.nodes, + matrix.rows[r].egress ); - }.bind(this) - ); - // for each link between routers that has traffic, start an animation - let keep = {}; - for (let id in hops) { - let hop = hops[id]; - for (let h = 0; h < hop.length; h++) { - let ahop = hop[h]; - let pathId = id.replace(/\./g, "\\.").replace(/ /g, "\\ "); - let flowId = - id.replace(/\./g, "").replace(/ /g, "") + - "-" + - this.addressIndex(this, ahop.address) + - (ahop.back ? "b" : ""); - let path = d3.select("#path" + pathId); - if (!path.empty()) { - // start the animation. If the animation is already running, this will have no effect - this.startAnimation(path, flowId, ahop, flowScale(ahop.val)); + let t = this.nodeIndexFor( + this.traffic.topology.nodes.nodes, + matrix.rows[r].ingress + ); + let address = matrix.getAddress(r, c); + if (r !== c) { + // accumulate the hops between the ingress and egress routers + nextHop( + this.traffic.topology.nodes.nodes[f], + this.traffic.topology.nodes.nodes[t], + this.traffic.topology.nodes, + this.traffic.topology.links, + this.traffic.QDRService.management.topology.nodeInfo(), + this.traffic.topology.nodes.nodes[f], + function(link, fnode, tnode) { + let key = "-" + link.uid(); + let back = fnode.index < tnode.index; + if (!hops[key]) hops[key] = []; + hops[key].push({ + val: val, + back: back, + address: address + }); + } + ); } - keep[flowId] = true; + // Find the senders connected to nodes[f] and the receivers connected to nodes[t] + // and add their links to the animation + this.addClients(hops, f, val, true, address); + this.addClients(hops, t, val, false, address); + + const ftrue = this.addEdges(hops, f, val, true, address); + const ffalse = this.addEdges(hops, f, val, false, address); + const ttrue = this.addEdges(hops, t, val, true, address); + const tfalse = this.addEdges(hops, t, val, false, address); + [...ftrue, ...ffalse, ...ttrue, ...tfalse].forEach(eIndex => { + this.addClients(hops, eIndex, val, true, address); + this.addClients(hops, eIndex, val, false, address); + }); } - } - // remove any existing animations that we don't have data for anymore - for (let id in this.lastFlows) { - if (this.lastFlows[id] && !keep[id]) { - this.lastFlows[id] = 0; - d3.select("#SVG_ID") - .selectAll("circle.flow" + id) - .remove(); + }); + }); + // for each link between routers that has traffic, start an animation + let keep = {}; + let offset = 0; // index of the address going in the same driection + let total = 0; // number of addresses going in the same direction for this path + + for (let id in hops) { + let hop = hops[id]; + const backCount = hop.filter(h => h.back).length; + const foreCount = hop.length - backCount; + let backIndex = 0; // index of address going the back direction + let foreIndex = 0; + for (let h = 0; h < hop.length; h++) { + let ahop = hop[h]; + let pathId = id.replace(/\./g, "\\.").replace(/ /g, "\\ "); + let flowId = + id.replace(/\./g, "").replace(/ /g, "") + + "-" + + this.addressIndex(ahop.address) + + (ahop.back ? "b" : ""); + let path = d3.select("#path" + pathId); + if (!path.empty()) { + if (ahop.back) { + offset = backIndex++; + total = backCount; + } else { + offset = foreIndex++; + total = foreCount; + } + // start the animation. If the animation is already running, this will have no effect + this.animateDots(path, flowId, ahop, flowScale(ahop.val), offset, total); } + keep[flowId] = true; } } - } - // animate the d3 selection (flow) along the given path - animateFlow(flow, path, count, back, rate) { + // remove any existing animations that we don't have data for anymore + for (let id in this.lastFlows) { + if (this.lastFlows[id] && !keep[id]) { + this.lastFlows[id] = 0; + d3.select("#SVG_ID") + .selectAll("circle.flow" + id) + .remove(); + } + } + }; + + // move the dots in the selection flow along the path + animateFlow(flow, path, back, rate, offset, total) { let l = path.node().getTotalLength(); + const count = flow.size(); + const duration = (l * 10) / rate; flow .transition() .ease("easeLinear") - .duration((l * 10) / rate) - .attrTween("transform", this.translateDots(this.radius, path, count, back)) + .duration(duration) + .attrTween("transform", this.translateDots(path, count, back, offset, total)) .each("end", () => { if (this.stopped === false) { - this.animateFlow(flow, path, count, back, rate); + setTimeout(() => { + this.animateFlow(flow, path, back, rate, offset, total); + }, 1); } }); } - // create dots along the path between routers - startAnimation(selection, id, hop, rate) { - if (selection.empty()) return; - this.animateDots(selection, id, hop, rate); - } - animateDots(path, id, hop, rate) { + + animateDots(path, id, hop, rate, offset, total) { let back = hop.back, address = hop.address; // the density of dots is determined by the rate of this traffic relative to the other traffic if (!path.node().getTotalLength) return; let len = Math.max(Math.floor(path.node().getTotalLength() / 50), 1); let dots = []; - for (let i = 0, offset = this.addressIndex(this, address); i < len; ++i) { + for (let i = 0, offset = this.addressIndex(address); i < len; ++i) { dots[i] = { i: i + 10 * offset, - address: address + address }; } // keep track of the number of dots for each link. If the length of the link is changed, // re-create the animation - if (!this.lastFlows[id]) this.lastFlows[id] = len; - else { + if (!this.lastFlows[id]) { + this.lastFlows[id] = len; + } else { if (this.lastFlows[id] !== len) { this.lastFlows[id] = len; d3.select("#SVG_ID") @@ -502,27 +561,31 @@ class Dots extends TrafficAnimation { .append("circle") .attr("class", "flow flow" + id) .attr("data-testid", (d, i) => `flow${id}-${i}`) - .attr("fill", this.fillColor(address, this.traffic.addressColors)) + .attr("fill", this.fillColor(address)) .attr("r", 4); - this.animateFlow(circles, path, dots.length, back, rate); + + // start the animation + if (circles.size() > 0) { + this.animateFlow(circles, path, back, rate, offset, total); + } flow.exit().remove(); } - fillColor(n, addressColors) { - if (!(n in addressColors)) { - let ci = Object.keys(addressColors).length; - addressColors[n] = this.colorGen(ci); - } - return addressColors[n]; + + fillColor(address) { + return colorGen(address); } + // find the link that carries traffic for this address // going to nodes[f] if sender is true // coming from nodes[f] if sender if false. // Add the link's id to the hops array - addClients(hops, nodes, f, val, sender, address) { + addClients(hops, f, val, sender, address) { + const nodes = this.traffic.topology.nodes.nodes; if (!nodes[f]) return; const cdir = sender ? "out" : "in"; const uuid = nodes[f].uid(); const key = nodes[f].key; + if (!this.traffic.QDRService.management.topology._nodeInfo[key]) return; const links = this.traffic.QDRService.management.topology._nodeInfo[key][ "router.link" ]; @@ -538,7 +601,6 @@ class Dots extends TrafficAnimation { l[ild] === cdir ); }, this); - // we now have the links involved in traffic for this address that // ingress/egress to/from this router (f). // Now find the created node that each link is associated with @@ -559,7 +621,7 @@ class Dots extends TrafficAnimation { hops[key].push({ val: val, back: !sender, - address: address + address }); return true; } @@ -568,19 +630,84 @@ class Dots extends TrafficAnimation { } } } - addressIndex(vis, address) { - return Object.keys(vis.traffic.addresses).indexOf(address); + + addEdges(hops, f, val, sender, address) { + const nodes = this.traffic.topology.nodes.nodes; + if (!nodes[f]) return; + const edges = []; + const cdir = sender ? "out" : "in"; + const uuid = nodes[f].uid(); + const key = nodes[f].key; + const links = this.traffic.QDRService.management.topology._nodeInfo[key][ + "router.link" + ]; + if (links) { + const ilt = links.attributeNames.indexOf("linkType"); + const ioa = links.attributeNames.indexOf("owningAddr"); + const ici = links.attributeNames.indexOf("connectionId"); + const ild = links.attributeNames.indexOf("linkDir"); + let foundLinks = links.results.filter(function(l) { + return ( + (l[ilt] === "endpoint" || l[ilt] === "edge-downlink") && + address === utils.addr_text(l[ioa]) && + l[ild] === cdir + ); + }, this); + + // we now have the links involved in traffic for this address that + // ingress/egress to/from this router (f). + // Now find the edge router for the links + const connections = this.traffic.QDRService.management.topology._nodeInfo[key][ + "connection" + ]; + for (let linkIndex = 0; linkIndex < foundLinks.length; linkIndex++) { + const connectionId = foundLinks[linkIndex][ici]; + const iconnid = connections.attributeNames.indexOf("identity"); + const connection = connections.results.find( + result => result[iconnid] === connectionId + ); + if (connection) { + const container = utils.valFor( + connections.attributeNames, + connection, + "container" + ); + const edge = nodes.find( + node => node.nodeType === "_edge" && node.name === container + ); + if (edge) { + const uuid2 = edge.uid(); + const key = ["", uuid, uuid2].join("-"); + if (!hops[key]) hops[key] = []; + hops[key].push({ + val: val, + back: !sender, + address + }); + edges.push(edge.index); + } + } + } + } + return edges; + } + + addressIndex(address) { + return Object.keys(addressColors).indexOf(address); } + // calculate the translation for each dot along the path - translateDots(radius, path, count, back) { + translateDots(path, count, back, offset, total) { let pnode = path.node(); // will be called for each element in the flow selection (for each dot) return function(d) { + const off = offset / total / count; // will be called with t going from 0 to 1 for each dot return function(t) { // start the points at different positions depending on their value (d) - let tt = t * 1000; - let f = ((tt + (d.i * 1000) / count) % 1000) / 1000; + let tt = t * 1000; // total time + let f = ((tt + (d.i * 1000) / count) % 1000) / 1000; // fraction + f = (f + off) % 1; if (back) f = 1 - f; // l needs to be calculated each tick because the path's length might be changed during the animation let l = pnode.getTotalLength(); --------------------------------------------------------------------- To unsubscribe, e-mail: commits-unsubscr...@qpid.apache.org For additional commands, e-mail: commits-h...@qpid.apache.org