Built with blockbuilder.org
Last active
November 15, 2018 15:15
-
-
Save mforando/4b14fcc3f6da2ca66572dc630c5e7aa2 to your computer and use it in GitHub Desktop.
Edge Bundling
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
license: mit |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<script src="http://d3js.org/d3.v4.min.js" charset="utf-8"></script> | |
<style type="text/css"> | |
body { | |
margin: 0; | |
font-family: sans-serif; | |
font-size: 11px; | |
} | |
.axis path, .axis line { | |
fill: none; | |
stroke: black; | |
shape-rendering: crispEdges; | |
} | |
circle:hover{ | |
fill: green; | |
} | |
#imgOverlay{ | |
position:fixed; | |
top:0px; | |
z-index:-99; | |
pointer-events:none; | |
} | |
#svgcontainer{ | |
z-index:1; | |
border:1px solid black; | |
} | |
</style> | |
</head> | |
<div class="slidecontainer"> | |
<label>Edge Bundling: </label><input type="range" min="0" max="1" value=".1" step=".005" class="slider" id="bundlefactor"><span> ▌▌</span><span> ▶</span><br> | |
</div> | |
<body> | |
<div id="container"> | |
<img id="imgOverlay" src="https://i.pinimg.com/originals/21/23/58/21235851a186da53800cee6be5b26412.jpg"> | |
</img> | |
</div> | |
<script> | |
/* | |
FDEB algorithm implementation [www.win.tue.nl/~dholten/papers/forcebundles_eurovis.pdf]. | |
Author: Corneliu S. (github.com/upphiminn) | |
2013 | |
*/ | |
(function () { | |
d3.ForceEdgeBundling = function () { | |
var data_nodes = {}, // {'nodeid':{'x':,'y':},..} | |
data_edges = [], // [{'source':'nodeid1', 'target':'nodeid2'},..] | |
compatibility_list_for_edge = [], | |
subdivision_points_for_edge = [], | |
K = 0.1, // global bundling constant controlling edge stiffness | |
S_initial = 0.1, // init. distance to move points | |
P_initial = 1, // init. subdivision number | |
P_rate = 2, // subdivision rate increase | |
C = 6, // number of cycles to perform | |
I_initial = 90, // init. number of iterations for cycle | |
I_rate = 0.6666667, // rate at which iteration number decreases i.e. 2/3 | |
compatibility_threshold = 0.6, | |
eps = 1e-6; | |
/*** Geometry Helper Methods ***/ | |
function vector_dot_product(p, q) { | |
return p.x * q.x + p.y * q.y; | |
} | |
function edge_as_vector(P) { | |
return { | |
'x': data_nodes[P.target].x - data_nodes[P.source].x, | |
'y': data_nodes[P.target].y - data_nodes[P.source].y | |
} | |
} | |
function edge_length(e) { | |
// handling nodes that are on the same location, so that K/edge_length != Inf | |
if (Math.abs(data_nodes[e.source].x - data_nodes[e.target].x) < eps && | |
Math.abs(data_nodes[e.source].y - data_nodes[e.target].y) < eps) { | |
return eps; | |
} | |
return Math.sqrt(Math.pow(data_nodes[e.source].x - data_nodes[e.target].x, 2) + | |
Math.pow(data_nodes[e.source].y - data_nodes[e.target].y, 2)); | |
} | |
function custom_edge_length(e) { | |
return Math.sqrt(Math.pow(e.source.x - e.target.x, 2) + Math.pow(e.source.y - e.target.y, 2)); | |
} | |
function edge_midpoint(e) { | |
var middle_x = (data_nodes[e.source].x + data_nodes[e.target].x) / 2.0; | |
var middle_y = (data_nodes[e.source].y + data_nodes[e.target].y) / 2.0; | |
return { | |
'x': middle_x, | |
'y': middle_y | |
}; | |
} | |
function compute_divided_edge_length(e_idx) { | |
var length = 0; | |
for (var i = 1; i < subdivision_points_for_edge[e_idx].length; i++) { | |
var segment_length = euclidean_distance(subdivision_points_for_edge[e_idx][i], subdivision_points_for_edge[e_idx][i - 1]); | |
length += segment_length; | |
} | |
return length; | |
} | |
function euclidean_distance(p, q) { | |
return Math.sqrt(Math.pow(p.x - q.x, 2) + Math.pow(p.y - q.y, 2)); | |
} | |
function project_point_on_line(p, Q) { | |
var L = Math.sqrt((Q.target.x - Q.source.x) * (Q.target.x - Q.source.x) + (Q.target.y - Q.source.y) * (Q.target.y - Q.source.y)); | |
var r = ((Q.source.y - p.y) * (Q.source.y - Q.target.y) - (Q.source.x - p.x) * (Q.target.x - Q.source.x)) / (L * L); | |
return { | |
'x': (Q.source.x + r * (Q.target.x - Q.source.x)), | |
'y': (Q.source.y + r * (Q.target.y - Q.source.y)) | |
}; | |
} | |
/*** ********************** ***/ | |
/*** Initialization Methods ***/ | |
function initialize_edge_subdivisions() { | |
for (var i = 0; i < data_edges.length; i++) { | |
if (P_initial === 1) { | |
subdivision_points_for_edge[i] = []; //0 subdivisions | |
} else { | |
subdivision_points_for_edge[i] = []; | |
subdivision_points_for_edge[i].push(data_nodes[data_edges[i].source]); | |
subdivision_points_for_edge[i].push(data_nodes[data_edges[i].target]); | |
} | |
} | |
} | |
function initialize_compatibility_lists() { | |
for (var i = 0; i < data_edges.length; i++) { | |
compatibility_list_for_edge[i] = []; //0 compatible edges. | |
} | |
} | |
function filter_self_loops(edgelist) { | |
var filtered_edge_list = []; | |
for (var e = 0; e < edgelist.length; e++) { | |
if (data_nodes[edgelist[e].source].x != data_nodes[edgelist[e].target].x || | |
data_nodes[edgelist[e].source].y != data_nodes[edgelist[e].target].y) { //or smaller than eps | |
filtered_edge_list.push(edgelist[e]); | |
} | |
} | |
return filtered_edge_list; | |
} | |
/*** ********************** ***/ | |
/*** Force Calculation Methods ***/ | |
function apply_spring_force(e_idx, i, kP) { | |
var prev = subdivision_points_for_edge[e_idx][i - 1]; | |
var next = subdivision_points_for_edge[e_idx][i + 1]; | |
var crnt = subdivision_points_for_edge[e_idx][i]; | |
var x = prev.x - crnt.x + next.x - crnt.x; | |
var y = prev.y - crnt.y + next.y - crnt.y; | |
x *= kP; | |
y *= kP; | |
return { | |
'x': x, | |
'y': y | |
}; | |
} | |
function apply_electrostatic_force(e_idx, i) { | |
var sum_of_forces = { | |
'x': 0, | |
'y': 0 | |
}; | |
var compatible_edges_list = compatibility_list_for_edge[e_idx]; | |
for (var oe = 0; oe < compatible_edges_list.length; oe++) { | |
var force = { | |
'x': subdivision_points_for_edge[compatible_edges_list[oe]][i].x - subdivision_points_for_edge[e_idx][i].x, | |
'y': subdivision_points_for_edge[compatible_edges_list[oe]][i].y - subdivision_points_for_edge[e_idx][i].y | |
}; | |
if ((Math.abs(force.x) > eps) || (Math.abs(force.y) > eps)) { | |
var diff = (1 / Math.pow(custom_edge_length({ | |
'source': subdivision_points_for_edge[compatible_edges_list[oe]][i], | |
'target': subdivision_points_for_edge[e_idx][i] | |
}), 1)); | |
sum_of_forces.x += force.x * diff; | |
sum_of_forces.y += force.y * diff; | |
} | |
} | |
return sum_of_forces; | |
} | |
function apply_resulting_forces_on_subdivision_points(e_idx, P, S) { | |
var kP = K / (edge_length(data_edges[e_idx]) * (P + 1)); // kP=K/|P|(number of segments), where |P| is the initial length of edge P. | |
// (length * (num of sub division pts - 1)) | |
var resulting_forces_for_subdivision_points = [{ | |
'x': 0, | |
'y': 0 | |
}]; | |
for (var i = 1; i < P + 1; i++) { // exclude initial end points of the edge 0 and P+1 | |
var resulting_force = { | |
'x': 0, | |
'y': 0 | |
}; | |
spring_force = apply_spring_force(e_idx, i, kP); | |
electrostatic_force = apply_electrostatic_force(e_idx, i, S); | |
resulting_force.x = S * (spring_force.x + electrostatic_force.x); | |
resulting_force.y = S * (spring_force.y + electrostatic_force.y); | |
resulting_forces_for_subdivision_points.push(resulting_force); | |
} | |
resulting_forces_for_subdivision_points.push({ | |
'x': 0, | |
'y': 0 | |
}); | |
return resulting_forces_for_subdivision_points; | |
} | |
/*** ********************** ***/ | |
/*** Edge Division Calculation Methods ***/ | |
function update_edge_divisions(P) { | |
for (var e_idx = 0; e_idx < data_edges.length; e_idx++) { | |
if (P === 1) { | |
subdivision_points_for_edge[e_idx].push(data_nodes[data_edges[e_idx].source]); // source | |
subdivision_points_for_edge[e_idx].push(edge_midpoint(data_edges[e_idx])); // mid point | |
subdivision_points_for_edge[e_idx].push(data_nodes[data_edges[e_idx].target]); // target | |
} else { | |
var divided_edge_length = compute_divided_edge_length(e_idx); | |
var segment_length = divided_edge_length / (P + 1); | |
var current_segment_length = segment_length; | |
var new_subdivision_points = []; | |
new_subdivision_points.push(data_nodes[data_edges[e_idx].source]); //source | |
for (var i = 1; i < subdivision_points_for_edge[e_idx].length; i++) { | |
var old_segment_length = euclidean_distance(subdivision_points_for_edge[e_idx][i], subdivision_points_for_edge[e_idx][i - 1]); | |
while (old_segment_length > current_segment_length) { | |
var percent_position = current_segment_length / old_segment_length; | |
var new_subdivision_point_x = subdivision_points_for_edge[e_idx][i - 1].x; | |
var new_subdivision_point_y = subdivision_points_for_edge[e_idx][i - 1].y; | |
new_subdivision_point_x += percent_position * (subdivision_points_for_edge[e_idx][i].x - subdivision_points_for_edge[e_idx][i - 1].x); | |
new_subdivision_point_y += percent_position * (subdivision_points_for_edge[e_idx][i].y - subdivision_points_for_edge[e_idx][i - 1].y); | |
new_subdivision_points.push({ | |
'x': new_subdivision_point_x, | |
'y': new_subdivision_point_y | |
}); | |
old_segment_length -= current_segment_length; | |
current_segment_length = segment_length; | |
} | |
current_segment_length -= old_segment_length; | |
} | |
new_subdivision_points.push(data_nodes[data_edges[e_idx].target]); //target | |
subdivision_points_for_edge[e_idx] = new_subdivision_points; | |
} | |
} | |
} | |
/*** ********************** ***/ | |
/*** Edge compatibility measures ***/ | |
function angle_compatibility(P, Q) { | |
return Math.abs(vector_dot_product(edge_as_vector(P), edge_as_vector(Q)) / (edge_length(P) * edge_length(Q))); | |
} | |
function scale_compatibility(P, Q) { | |
var lavg = (edge_length(P) + edge_length(Q)) / 2.0; | |
return 2.0 / (lavg / Math.min(edge_length(P), edge_length(Q)) + Math.max(edge_length(P), edge_length(Q)) / lavg); | |
} | |
function position_compatibility(P, Q) { | |
var lavg = (edge_length(P) + edge_length(Q)) / 2.0; | |
var midP = { | |
'x': (data_nodes[P.source].x + data_nodes[P.target].x) / 2.0, | |
'y': (data_nodes[P.source].y + data_nodes[P.target].y) / 2.0 | |
}; | |
var midQ = { | |
'x': (data_nodes[Q.source].x + data_nodes[Q.target].x) / 2.0, | |
'y': (data_nodes[Q.source].y + data_nodes[Q.target].y) / 2.0 | |
}; | |
return lavg / (lavg + euclidean_distance(midP, midQ)); | |
} | |
function edge_visibility(P, Q) { | |
var I0 = project_point_on_line(data_nodes[Q.source], { | |
'source': data_nodes[P.source], | |
'target': data_nodes[P.target] | |
}); | |
var I1 = project_point_on_line(data_nodes[Q.target], { | |
'source': data_nodes[P.source], | |
'target': data_nodes[P.target] | |
}); //send actual edge points positions | |
var midI = { | |
'x': (I0.x + I1.x) / 2.0, | |
'y': (I0.y + I1.y) / 2.0 | |
}; | |
var midP = { | |
'x': (data_nodes[P.source].x + data_nodes[P.target].x) / 2.0, | |
'y': (data_nodes[P.source].y + data_nodes[P.target].y) / 2.0 | |
}; | |
return Math.max(0, 1 - 2 * euclidean_distance(midP, midI) / euclidean_distance(I0, I1)); | |
} | |
function visibility_compatibility(P, Q) { | |
return Math.min(edge_visibility(P, Q), edge_visibility(Q, P)); | |
} | |
function compatibility_score(P, Q) { | |
return (angle_compatibility(P, Q) * scale_compatibility(P, Q) * position_compatibility(P, Q) * visibility_compatibility(P, Q)); | |
} | |
function are_compatible(P, Q) { | |
return (compatibility_score(P, Q) >= compatibility_threshold); | |
} | |
function compute_compatibility_lists() { | |
for (var e = 0; e < data_edges.length - 1; e++) { | |
for (var oe = e + 1; oe < data_edges.length; oe++) { // don't want any duplicates | |
if (are_compatible(data_edges[e], data_edges[oe])) { | |
compatibility_list_for_edge[e].push(oe); | |
compatibility_list_for_edge[oe].push(e); | |
} | |
} | |
} | |
} | |
/*** ************************ ***/ | |
/*** Main Bundling Loop Methods ***/ | |
var forcebundle = function () { | |
var S = S_initial; | |
var I = I_initial; | |
var P = P_initial; | |
initialize_edge_subdivisions(); | |
initialize_compatibility_lists(); | |
update_edge_divisions(P); | |
compute_compatibility_lists(); | |
for (var cycle = 0; cycle < C; cycle++) { | |
for (var iteration = 0; iteration < I; iteration++) { | |
var forces = []; | |
for (var edge = 0; edge < data_edges.length; edge++) { | |
forces[edge] = apply_resulting_forces_on_subdivision_points(edge, P, S); | |
} | |
for (var e = 0; e < data_edges.length; e++) { | |
for (var i = 0; i < P + 1; i++) { | |
subdivision_points_for_edge[e][i].x += forces[e][i].x; | |
subdivision_points_for_edge[e][i].y += forces[e][i].y; | |
} | |
} | |
} | |
// prepare for next cycle | |
S = S / 2; | |
P = P * P_rate; | |
I = I_rate * I; | |
update_edge_divisions(P); | |
//console.log('C' + cycle); | |
//console.log('P' + P); | |
//console.log('S' + S); | |
} | |
return subdivision_points_for_edge; | |
}; | |
/*** ************************ ***/ | |
/*** Getters/Setters Methods ***/ | |
forcebundle.nodes = function (nl) { | |
if (arguments.length === 0) { | |
return data_nodes; | |
} else { | |
data_nodes = nl; | |
} | |
return forcebundle; | |
}; | |
forcebundle.edges = function (ll) { | |
if (arguments.length === 0) { | |
return data_edges; | |
} else { | |
data_edges = filter_self_loops(ll); //remove edges to from to the same point | |
} | |
return forcebundle; | |
}; | |
forcebundle.bundling_stiffness = function (k) { | |
if (arguments.length === 0) { | |
return K; | |
} else { | |
K = k; | |
} | |
return forcebundle; | |
}; | |
forcebundle.step_size = function (step) { | |
if (arguments.length === 0) { | |
return S_initial; | |
} else { | |
S_initial = step; | |
} | |
return forcebundle; | |
}; | |
forcebundle.cycles = function (c) { | |
if (arguments.length === 0) { | |
return C; | |
} else { | |
C = c; | |
} | |
return forcebundle; | |
}; | |
forcebundle.iterations = function (i) { | |
if (arguments.length === 0) { | |
return I_initial; | |
} else { | |
I_initial = i; | |
} | |
return forcebundle; | |
}; | |
forcebundle.iterations_rate = function (i) { | |
if (arguments.length === 0) { | |
return I_rate; | |
} else { | |
I_rate = i; | |
} | |
return forcebundle; | |
}; | |
forcebundle.subdivision_points_seed = function (p) { | |
if (arguments.length == 0) { | |
return P; | |
} else { | |
P = p; | |
} | |
return forcebundle; | |
}; | |
forcebundle.subdivision_rate = function (r) { | |
if (arguments.length === 0) { | |
return P_rate; | |
} else { | |
P_rate = r; | |
} | |
return forcebundle; | |
}; | |
forcebundle.compatibility_threshold = function (t) { | |
if (arguments.length === 0) { | |
return compatibility_threshold; | |
} else { | |
compatibility_threshold = t; | |
} | |
return forcebundle; | |
}; | |
/*** ************************ ***/ | |
return forcebundle; | |
} | |
})(); | |
</script> | |
<script type="text/javascript"> | |
d3.sankey = function() { | |
var sankey = {}, | |
nodeWidth = 24, | |
nodePadding = 8, | |
size = [1, 1], | |
nodes = [], | |
links = [], | |
sinksRight = true; | |
sankey.nodeWidth = function(_) { | |
if (!arguments.length) return nodeWidth; | |
nodeWidth = +_; | |
return sankey; | |
}; | |
sankey.nodePadding = function(_) { | |
if (!arguments.length) return nodePadding; | |
nodePadding = +_; | |
return sankey; | |
}; | |
sankey.nodes = function(_) { | |
if (!arguments.length) return nodes; | |
nodes = _; | |
return sankey; | |
}; | |
sankey.links = function(_) { | |
if (!arguments.length) return links; | |
links = _; | |
return sankey; | |
}; | |
sankey.size = function(_) { | |
if (!arguments.length) return size; | |
size = _; | |
return sankey; | |
}; | |
sankey.sinksRight = function (_) { | |
if (!arguments.length) return sinksRight; | |
sinksRight = _; | |
return sankey; | |
}; | |
sankey.layout = function(iterations) { | |
computeNodeLinks(); | |
computeNodeValues(); | |
computeNodeBreadths(); | |
computeNodeDepths(iterations); | |
return sankey; | |
}; | |
sankey.relayout = function() { | |
computeLinkDepths(); | |
return sankey; | |
}; | |
// SVG path data generator, to be used as "d" attribute on "path" element selection. | |
sankey.link = function() { | |
var curvature = .5; | |
function link(d) { | |
var xs = d.source.x + d.source.dx, | |
xt = d.target.x, | |
xi = d3.interpolateNumber(xs, xt), | |
xsc = xi(curvature), | |
xtc = xi(1 - curvature), | |
ys = d.source.y + d.sy + d.dy / 2, | |
yt = d.target.y + d.ty + d.dy / 2; | |
if (!d.cycleBreaker) { | |
return "M" + xs + "," + ys | |
+ "C" + xsc + "," + ys | |
+ " " + xtc + "," + yt | |
+ " " + xt + "," + yt; | |
} else { | |
var xdelta = (1.5 * d.dy + 0.05 * Math.abs(xs - xt)); | |
xsc = xs + xdelta; | |
xtc = xt - xdelta; | |
var xm = xi(0.5); | |
var ym = d3.interpolateNumber(ys, yt)(0.5); | |
var ydelta = (2 * d.dy + 0.1 * Math.abs(xs - xt) + 0.1 * Math.abs(ys - yt)) * (ym < (size[1] / 2) ? -1 : 1); | |
return "M" + xs + "," + ys | |
+ "C" + xsc + "," + ys | |
+ " " + xsc + "," + (ys + ydelta) | |
+ " " + xm + "," + (ym + ydelta) | |
+ "S" + xtc + "," + yt | |
+ " " + xt + "," + yt; | |
} | |
} | |
link.curvature = function(_) { | |
if (!arguments.length) return curvature; | |
curvature = +_; | |
return link; | |
}; | |
return link; | |
}; | |
// Populate the sourceLinks and targetLinks for each node. | |
// Also, if the source and target are not objects, assume they are indices. | |
function computeNodeLinks() { | |
nodes.forEach(function(node) { | |
// Links that have this node as source. | |
node.sourceLinks = []; | |
// Links that have this node as target. | |
node.targetLinks = []; | |
}); | |
links.forEach(function(link) { | |
var source = link.source, | |
target = link.target; | |
if (typeof source === "number") source = link.source = nodes[link.source]; | |
if (typeof target === "number") target = link.target = nodes[link.target]; | |
source.sourceLinks.push(link); | |
target.targetLinks.push(link); | |
}); | |
} | |
// Compute the value (size) of each node by summing the associated links. | |
function computeNodeValues() { | |
nodes.forEach(function(node) { | |
node.value = Math.max( | |
d3.sum(node.sourceLinks, value), | |
d3.sum(node.targetLinks, value) | |
); | |
}); | |
} | |
// Iteratively assign the breadth (x-position) for each node. | |
// Nodes are assigned the maximum breadth of incoming neighbors plus one; | |
// nodes with no incoming links are assigned breadth zero, while | |
// nodes with no outgoing links are assigned the maximum breadth. | |
function computeNodeBreadths() { | |
var remainingNodes = nodes, | |
nextNodes, | |
x = 0; | |
// Work from left to right. | |
// Keep updating the breath (x-position) of nodes that are target of recently updated nodes. | |
while (remainingNodes.length && x < nodes.length) { | |
nextNodes = []; | |
remainingNodes.forEach(function(node) { | |
node.x = x; | |
node.dx = nodeWidth; | |
node.sourceLinks.forEach(function(link) { | |
if (nextNodes.indexOf(link.target) < 0 && !link.cycleBreaker) { | |
nextNodes.push(link.target); | |
} | |
}); | |
}); | |
if (nextNodes.length == remainingNodes.length) { | |
// There must be a cycle here. Let's search for a link that breaks it. | |
findAndMarkCycleBreaker(nextNodes); | |
// Start over. | |
// TODO: make this optional? | |
return computeNodeBreadths(); | |
} | |
else { | |
remainingNodes = nextNodes; | |
++x; | |
} | |
} | |
// Optionally move pure sinks always to the right. | |
if (sinksRight) { | |
moveSinksRight(x); | |
} | |
scaleNodeBreadths((size[0] - nodeWidth) / (x - 1)); | |
} | |
// Find a link that breaks a cycle in the graph (if any). | |
function findAndMarkCycleBreaker(nodes) { | |
// Go through all nodes from the given subset and traverse links searching for cycles. | |
var link; | |
for (var n=nodes.length - 1; n >= 0; n--) { | |
link = depthFirstCycleSearch(nodes[n], []); | |
if (link) { | |
return link; | |
} | |
} | |
// Depth-first search to find a link that is part of a cycle. | |
function depthFirstCycleSearch(cursorNode, path) { | |
var target, link; | |
for (var n = cursorNode.sourceLinks.length - 1; n >= 0; n--) { | |
link = cursorNode.sourceLinks[n]; | |
if (link.cycleBreaker) { | |
// Skip already known cycle breakers. | |
continue; | |
} | |
// Check if target of link makes a cycle in current path. | |
target = link.target; | |
for (var l = 0; l < path.length; l++) { | |
if (path[l].source == target) { | |
// We found a cycle. Search for weakest link in cycle | |
var weakest = link; | |
for (; l < path.length; l++) { | |
if (path[l].value < weakest.value) { | |
weakest = path[l]; | |
} | |
} | |
// Mark weakest link as (known) cycle breaker and abort search. | |
weakest.cycleBreaker = true; | |
return weakest; | |
} | |
} | |
// Recurse deeper. | |
path.push(link); | |
link = depthFirstCycleSearch(target, path); | |
path.pop(); | |
// Stop further search if we found a cycle breaker. | |
if (link) { | |
return link; | |
} | |
} | |
} | |
} | |
function moveSourcesRight() { | |
nodes.forEach(function(node) { | |
if (!node.targetLinks.length) { | |
node.x = d3.min(node.sourceLinks, function(d) { return d.target.x; }) - 1; | |
} | |
}); | |
} | |
function moveSinksRight(x) { | |
nodes.forEach(function(node) { | |
if (!node.sourceLinks.length) { | |
node.x = x - 1; | |
} | |
}); | |
} | |
function scaleNodeBreadths(kx) { | |
nodes.forEach(function(node) { | |
node.x *= kx; | |
}); | |
} | |
// Compute the depth (y-position) for each node. | |
function computeNodeDepths(iterations) { | |
// Group nodes by breath. | |
var nodesByBreadth = d3.nest() | |
.key(function(d) { return d.x; }) | |
.sortKeys(d3.ascending) | |
.entries(nodes) | |
.map(function(d) { return d.values; }); | |
// | |
initializeNodeDepth(); | |
resolveCollisions(); | |
computeLinkDepths(); | |
for (var alpha = 1; iterations > 0; --iterations) { | |
relaxRightToLeft(alpha *= .99); | |
resolveCollisions(); | |
computeLinkDepths(); | |
relaxLeftToRight(alpha); | |
resolveCollisions(); | |
computeLinkDepths(); | |
} | |
function initializeNodeDepth() { | |
// Calculate vertical scaling factor. | |
var ky = d3.min(nodesByBreadth, function(nodes) { | |
return (size[1] - (nodes.length - 1) * nodePadding) / d3.sum(nodes, value); | |
}); | |
nodesByBreadth.forEach(function(nodes) { | |
nodes.forEach(function(node, i) { | |
node.y = i; | |
node.dy = node.value * ky; | |
}); | |
}); | |
links.forEach(function(link) { | |
link.dy = link.value * ky; | |
}); | |
} | |
function relaxLeftToRight(alpha) { | |
nodesByBreadth.forEach(function(nodes, breadth) { | |
nodes.forEach(function(node) { | |
if (node.targetLinks.length) { | |
// Value-weighted average of the y-position of source node centers linked to this node. | |
var y = d3.sum(node.targetLinks, weightedSource) / d3.sum(node.targetLinks, value); | |
node.y += (y - center(node)) * alpha; | |
} | |
}); | |
}); | |
function weightedSource(link) { | |
return (link.source.y + link.sy + link.dy / 2) * link.value; | |
} | |
} | |
function relaxRightToLeft(alpha) { | |
nodesByBreadth.slice().reverse().forEach(function(nodes) { | |
nodes.forEach(function(node) { | |
if (node.sourceLinks.length) { | |
// Value-weighted average of the y-positions of target nodes linked to this node. | |
var y = d3.sum(node.sourceLinks, weightedTarget) / d3.sum(node.sourceLinks, value); | |
node.y += (y - center(node)) * alpha; | |
} | |
}); | |
}); | |
function weightedTarget(link) { | |
return (link.target.y + link.ty + link.dy / 2) * link.value; | |
} | |
} | |
function resolveCollisions() { | |
nodesByBreadth.forEach(function(nodes) { | |
var node, | |
dy, | |
y0 = 0, | |
n = nodes.length, | |
i; | |
// Push any overlapping nodes down. | |
nodes.sort(ascendingDepth); | |
for (i = 0; i < n; ++i) { | |
node = nodes[i]; | |
dy = y0 - node.y; | |
if (dy > 0) node.y += dy; | |
y0 = node.y + node.dy + nodePadding; | |
} | |
// If the bottommost node goes outside the bounds, push it back up. | |
dy = y0 - nodePadding - size[1]; | |
if (dy > 0) { | |
y0 = node.y -= dy; | |
// Push any overlapping nodes back up. | |
for (i = n - 2; i >= 0; --i) { | |
node = nodes[i]; | |
dy = node.y + node.dy + nodePadding - y0; | |
if (dy > 0) node.y -= dy; | |
y0 = node.y; | |
} | |
} | |
}); | |
} | |
function ascendingDepth(a, b) { | |
return a.y - b.y; | |
} | |
} | |
// Compute y-offset of the source endpoint (sy) and target endpoints (ty) of links, | |
// relative to the source/target node's y-position. | |
function computeLinkDepths() { | |
nodes.forEach(function(node) { | |
node.sourceLinks.sort(ascendingTargetDepth); | |
node.targetLinks.sort(ascendingSourceDepth); | |
}); | |
nodes.forEach(function(node) { | |
var sy = 0, ty = 0; | |
node.sourceLinks.forEach(function(link) { | |
link.sy = sy; | |
sy += link.dy; | |
}); | |
node.targetLinks.forEach(function(link) { | |
link.ty = ty; | |
ty += link.dy; | |
}); | |
}); | |
function ascendingSourceDepth(a, b) { | |
return a.source.y - b.source.y; | |
} | |
function ascendingTargetDepth(a, b) { | |
return a.target.y - b.target.y; | |
} | |
} | |
// Y-position of the middle of a node. | |
function center(node) { | |
return node.y + node.dy / 2; | |
} | |
// Value property accessor. | |
function value(x) { | |
return x.value; | |
} | |
return sankey; | |
}; | |
</script> | |
<script type="text/javascript"> | |
var w = window.innerWidth, | |
h = window.innerHeight, | |
margin = { top: 40, right: 20, bottom: 20, left: 40 }, | |
radius = 6; | |
var nodes = [] | |
var links = [] | |
var graph = [] | |
var svg = d3.select("#container").append("svg").attr("width",w).attr("height",h) | |
.style("display","absolute") | |
.style("background","none") | |
var defs = svg.append("svg:defs") | |
defs.append("svg:marker") | |
.attr("id", "arrow-right") | |
.attr("refX", 3) | |
.attr("refY", 3) | |
.attr("markerWidth", 30) | |
.attr("markerHeight", 30) | |
.attr("orient", "auto") | |
.append("path") | |
.attr("d", "M 0 0 6 3 0 6 3") | |
.style("fill", "black"); | |
svg.append("svg:defs").append("svg:marker") | |
.attr("id", "arrow-left") | |
.attr("refX", 2) | |
.attr("refY", 2) | |
.attr("markerWidth", 30) | |
.attr("markerHeight", 30) | |
.attr("orient", "auto") | |
.append("path") | |
.attr("d", "M 0 0 4 2 0 4 2") | |
.style("fill", "black") | |
svg.on("click", function() { | |
var coords = d3.mouse(this); | |
var newData= { | |
x: Math.round(coords[0]), | |
y: Math.round(coords[1]) | |
}; | |
nodes.push(newData); | |
updateCircles() | |
console.log(nodes,links) | |
if(nodes.length>1){ | |
links.push({source:nodes[nodes.length-2].name,target:nodes[nodes.length-1].name,value:Math.random()}) | |
forceBundle() | |
// sankeyDraw() | |
} | |
graph = [] | |
graph.nodes = nodes | |
graph.links = links | |
}) | |
d3.select("#bundlefactor").on("change",forceBundle) | |
function updateCircles(){ | |
var circles = d3.select("svg").selectAll("circle").data(nodes) | |
circles.enter() | |
.append("circle") | |
.attr("cx",function(d,i){ | |
d.name = i; | |
return d.x}) | |
.attr("cy",function(d){return d.y}) | |
.attr("r",15) | |
.style("fill","black") | |
.transition() | |
.duration(500) | |
.ease(d3.easeCubic) | |
.attr("r",5) | |
} | |
function handleMouseOver(d, i) { // Add interactivity | |
// Use D3 to select element, change color and size | |
d3.select(this).attr("fill","orange") | |
.attr("r",radius * 2) | |
svg.append("text").attr("id", "t" + d.x + "-" + d.y + "-" + i) | |
.attr("x",function() { return xScale(d.x) - 30; }) | |
.attr("y",function() { return yScale(d.y) - 15; }) | |
.text(function() { | |
return [d.x, d.y]; // Value of the text | |
}); | |
} | |
function handleMouseOut(d, i) { | |
// Use D3 to select element, change color back to normal | |
d3.select(this).attr({ | |
fill: "black", | |
r: radius | |
}); | |
// Select text by id and then remove | |
d3.select("#t" + d.x + "-" + d.y + "-" + i).remove(); // Remove text location | |
} | |
function forceBundle(){ | |
var bundval = d3.select("#bundlefactor").property("value") | |
var fbundling = d3.ForceEdgeBundling() | |
.step_size(0.5) | |
.compatibility_threshold(bundval) | |
.nodes(graph.nodes) | |
.edges(graph.links); | |
var results = fbundling(); | |
console.log(results) | |
var edges = svg.selectAll(".edges").data(results) | |
edges.enter() | |
.append("path") | |
.attr("class","edges") | |
.merge(edges) | |
.transition() | |
.attr("d",function(d){ | |
return d3.line().x(function(d){return d.x}).y(function(d){return d.y})(d) | |
}) | |
.style("stroke-width", 4) | |
.style("stroke", "#ff2222") | |
.style("fill", "none") | |
.style('stroke-opacity',.36); //use opacity as blending | |
} | |
var linksGroup = svg.append("g").attr('class', 'links'); | |
var nodesGroup = svg.append("g").attr('class', 'nodes'); | |
// Set up Sankey object. | |
var sankey = d3.sankey() | |
.nodeWidth(30) | |
.nodePadding(5) | |
.size([w, h]); | |
var path = sankey.link(); | |
function sankeyDraw() { | |
sankey.nodes(graph.nodes) | |
.links(graph.links) | |
.layout(32); | |
console.log(graph) | |
var links = linksGroup.selectAll('.link') | |
.data(graph.links); | |
links.enter() | |
.append("path") | |
.attr('class', 'link'); | |
links.attr('d', path) | |
.style("fill","rgba(0,0,0,.3)") | |
.style("stroke","rgba(0,0,0,.3)") | |
.style("stroke-width", function (d) { | |
return Math.max(1, d.dy); | |
}); | |
links.classed('backwards',function (d) { return d.target.x < d.source.x; }); | |
links.append("title") | |
.text(function (d) { | |
return d.source.name + " to " + d.target.name + " = " + d.value; | |
}); | |
// Exit | |
links.exit().remove(); | |
// Draw the nodes. | |
var nodes = nodesGroup.selectAll('.node').data(graph.nodes); | |
// Enter | |
var nodesEnterSelection = nodes.enter() | |
.append("g") | |
.attr('class', 'node'); | |
nodesEnterSelection.append("rect") | |
.attr('width', sankey.nodeWidth()) | |
.append("title"); | |
nodesEnterSelection.append("text") | |
.attr('x', sankey.nodeWidth() / 2) | |
.attr('dy', ".35em") | |
.attr("text-anchor", "middle") | |
.attr('transform', null); | |
// Enter + Update | |
nodes | |
.attr('transform', function (d) { | |
return "translate(" + d.x + "," + d.y + ")"; | |
}); | |
nodes.select('rect') | |
.attr('height', function (d) { | |
return d.dy; | |
}) | |
.style('fill',"black") | |
.style('stroke', function (d) { | |
return d3.rgb(d.color).darker(2); | |
}); | |
nodes.select('rect').select('title') | |
.text(function (d) { | |
return d.name; | |
}); | |
nodes.select('text') | |
.attr('y', function (d) { | |
return d.dy / 2; | |
}) | |
.text(function (d) { | |
return d.name; | |
}); | |
// Exit | |
nodes.exit().remove(); | |
} | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment