"use strict"; class ParallelLinksExample { constructor(containerId) { var container = document.getElementById(containerId); var that = this; var MARGIN = 10, VIEW_WIDTH = Math.min(container.offsetHeight, container.offsetWidth) - 2 * MARGIN, HEIGHT = VIEW_WIDTH, WIDTH = VIEW_WIDTH; this.LINK_WIDTH = 2; // Define the data for the visualization. var graph = { "nodes": [{}, {}], "links": [{ "target": 1, "source": 0, color: "blue" }, { "target": 1, "source": 0, color: "red" }, { "target": 1, "source": 0, color: "green" }, { "target": 1, "source": 0, color: "orange" }] }; // Create an SVG container to hold the visualization var svg = d3.select(container) .append('svg') .classed('example-svg', true) .attr('width', WIDTH) .attr('height', HEIGHT); // Extract the nodes and links from the data. this.nodes = graph.nodes; this.links = graph.links; this.prepareLinks(); // Create a force layout object var force = d3.layout.force() .size([WIDTH, HEIGHT]) .nodes(this.nodes) .links(this.links) .linkDistance(WIDTH / 3.5); var drag = force.drag(); // Draw the links var link = svg.selectAll('.link') .data(this.links) .enter() .append('line') .classed('example-link', true); // Draw the nodes var node = svg.selectAll('.node') .data(this.nodes) .enter() .append('circle') .classed('example-node', true) .attr('r', WIDTH / 30) .call(drag); this.setCalculationExact(true); // Start the force simulation force.start(); /** * @decription tick event listener */ force.on('tick', function() { // Add some randomization to node location, for fun. node .attr('cx', function(d) { var rand = Math.floor(Math.random() * 3) - 1; return d.x += rand; }) .attr('cy', function(d) { var rand = Math.floor(Math.random() * 3) - 1; return d.y += rand; }); link .attr('x1', function(d) { return d.source.x; }) .attr('y1', function(d) { return d.source.y; }) .attr('x2', function(d) { return d.target.x; }) .attr('y2', function(d) { return d.target.y; }) .attr('stroke', function(d) { return d.color; }) .attr('transform', function(d) { var translation = that.calcTranslation(d.targetDistance, d.source, d.target); return `translate (${translation.dx}, ${translation.dy})`; }); }); /** * @description * Make the demo simulation permanent, by resuming it when it ends. */ force.on('end', function() { force.resume(); }); } /** * @param {number} targetDistance * @param {x,y} point0 * @param {x,y} point1, two points that define a line segmemt * @returns * a translation {dx,dy} from the given line segment, such that the distance * between the given line segment and the translated line segment equals * targetDistance */ static calcTranslationExact(targetDistance, point0, point1) { var x1_x0 = point1.x - point0.x, y1_y0 = point1.y - point0.y, x2_x0, y2_y0; if (y1_y0 === 0) { x2_x0 = 0; y2_y0 = targetDistance; } else { var angle = Math.atan((x1_x0) / (y1_y0)); x2_x0 = -targetDistance * Math.cos(angle); y2_y0 = targetDistance * Math.sin(angle); } return { dx: x2_x0, dy: y2_y0 }; } /** * @param {number} targetDistance * @param {x,y} point0 * @param {x,y} point1, two points that define a line segmemt * @returns * a translation {dx,dy} from the given line segment, such that the distance * between the given line segment and the translated line segment satisfies * the condition: targetDistance < distance < 1.42 * targetDistance */ static calcTranslationApproximate(targetDistance, point0, point1) { var x1_x0 = point1.x - point0.x, y1_y0 = point1.y - point0.y, x2_x0, y2_y0; if (targetDistance === 0) { x2_x0 = y2_y0 = 0; } else if (y1_y0 === 0 || Math.abs(x1_x0 / y1_y0) > 1) { y2_y0 = -targetDistance; x2_x0 = targetDistance * y1_y0 / x1_x0; } else { x2_x0 = targetDistance; y2_y0 = targetDistance * (-x1_x0) / y1_y0; } return { dx: x2_x0, dy: y2_y0 }; } /** * @description * Select calculation method: exact or approximate. * @param {boolean} on Set exact calculation */ setCalculationExact(on) { this.calcTranslation = (on ? ParallelLinksExample.calcTranslationExact : ParallelLinksExample.calcTranslationApproximate); } /** * @description * Build an index to help handle the case of multiple links between two nodes */ prepareLinks() { var that = this, linksFromNodes = {}; this.links.forEach(function(val, idx) { var sid = val.source, tid = val.targetID, key = (sid < tid ? sid + "," + tid : tid + "," + sid); if (linksFromNodes[key] === undefined) { linksFromNodes[key] = [idx]; val.multiIdx = 1; } else { val.multiIdx = linksFromNodes[key].push(idx); } // Calculate target link distance, from the index in the multiple-links array: // 1 -> 0, 2 -> 2, 3-> -2, 4 -> 4, 5 -> -4, ... val.targetDistance = (val.multiIdx % 2 === 0 ? val.multiIdx * that.LINK_WIDTH : (-val.multiIdx + 1) * that.LINK_WIDTH); }); } }