Forked from upphiminn‘s block: Example of d3-ForceEdgeBundling on US airline routes graph and updated for d3 v4.
See this map which uses similar techniques https://www.nytimes.com/interactive/2015/11/12/us/gun-traffickers-smuggling-state-gun-laws.html
TODO: move to a Javascript worker.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>FDEB US Airline Routes Example</title>
<script src="https://d3js.org/d3.v4.min.js" charset="utf-8"></script>
<script type="text/javascript" src="d3-ForceEdgeBundling.js"></script>
</head>
<body>
<div id="svg">
</div>
<script>
d3.xml("airlines.xml", function(xml) {
//Transform the XML data into a proper format used by the algorithm
var raw_edges = xml.documentElement.getElementsByTagName("edge");
var raw_nodes = xml.documentElement.getElementsByTagName("node");
var eedges = [];
var nnodes = {};
var min_x = Number.MAX_VALUE;
var max_x = 0;
var min_y = Number.MAX_VALUE;
var max_y = 0;
for(var i = 0; i < raw_nodes.length; i++){
var key = raw_nodes[i].getAttribute('id');
var x = Math.abs(parseFloat(raw_nodes[i].childNodes[1].firstChild.nodeValue));
var name = raw_nodes[i].childNodes[3].firstChild.nodeValue;
var y = Math.abs(parseFloat(raw_nodes[i].childNodes[5].firstChild.nodeValue));
nnodes[key] = {'x':x, 'y':y};
min_x = Math.min(min_x, x);
max_x = Math.max(max_x, x);
min_y = Math.min(min_y, y);
max_y = Math.max(max_y, y);
}
for(var i = 0; i < raw_edges.length; i++){
eedges.push({'source':raw_edges[i].getAttribute('source'),
'target':raw_edges[i].getAttribute('target')});
}
console.log('Nodes', nnodes);
console.log('Edges', eedges);
var new_scale_x = d3.scaleLinear().domain([min_x,max_x]).range([900,50]);
var new_scale_y = d3.scaleLinear().domain([min_y, max_y]).range([460,50]);
for(var i = 0; i < raw_nodes.length; i++){
nnodes[i].x = new_scale_x(nnodes[i].x);
nnodes[i].y = new_scale_y(nnodes[i].y);
}
//Run the FDEB algorithm using default values on the data
var fbundling = d3.ForceEdgeBundling().nodes(nnodes).edges(eedges);
var results = fbundling();
var svg = d3.select("#svg").append("svg")
.attr("width", 1000)
.attr("height", 600);
svg = svg.append('g');
svg.append('rect')
.attr('fill', '#111155')
.attr('width', 1000)
.attr('height',600);
svg.attr('transform', 'translate(20, 20)');
var d3line = d3.line()
.x(function(d){return d.x;})
.y(function(d){return d.y;});
//plot the data
for(var i = 0; i < results.length; i++){
svg.append("path").attr("d", d3line(results[i]))
.style("stroke-width", 0.5)
.style("stroke", "#ff2222")
.style("fill", "none")
.style('stroke-opacity',0.15);
}
//draw nodes
svg.selectAll('.node')
.data(d3.entries(nnodes))
.enter()
.append('circle')
.classed('node', true)
.attr('r', 2)
.attr('fill','#ffee00')
.attr('cx', function(d){ return d.value.x;})
.attr('cy', function(d){ return d.value.y;});
});
</script>
</body>
</html>
/*
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 controling 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 = 70, // init. number of iterations for cycle
I_rate = 0.6666667, // rate at which iteration number decreases i.e. 2/3
compatibility_threshold = 0.6,
invers_quadratic_mode = false,
eps = 1e-8;
/*** 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){
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 , S){
var sum_of_forces = { 'x' : 0, 'y' : 0};
var compatible_edges_list = compatibility_list_for_edge[e_idx];
window.sbd = subdivision_points_for_edge;
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){
var result = Math.abs(vector_dot_product(edge_as_vector(P),edge_as_vector(Q))/(edge_length(P)*edge_length(Q)));
return result;
}
function scale_compatibility(P, Q){
var lavg = (edge_length(P) + edge_length(Q))/2.0;
var result = 2.0/(lavg/Math.min(edge_length(P),edge_length(Q)) + Math.max(edge_length(P), edge_length(Q))/lavg);
return result;
}
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};
var result = lavg/(lavg + euclidean_distance(midP, midQ));
return result;
}
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 acutal 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};
var result = Math.max(0, 1 - 2 * euclidean_distance(midP,midI)/euclidean_distance(I0,I1));
return result;
}
function visibility_compatibility(P, Q){
return Math.min(edge_visibility(P,Q), edge_visibility(Q,P));
}
function compatibility_score(P, Q){
var result = (angle_compatibility(P,Q) * scale_compatibility(P,Q) *
position_compatibility(P,Q) * visibility_compatibility(P,Q));
return result;
}
function are_compatible(P, Q){
//console.log('compatibility ' + P.source +' - '+ P.target + ' and ' + Q.source +' '+ Q.target);
return (compatibility_score(P,Q) >= compatibility_threshold);
}
function compute_compatibility_lists()
{
for(e = 0; e < data_edges.length - 1; e++){
for( oe = e + 1 ; oe < data_edges.length; oe++){ // don't want any duplicates
if(e == oe)
continue;
else{
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 * 2;
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.compatbility_threshold = function(t){
if(arguments.length == 0){
return compatbility_threshold;
}
else{
compatibility_threshold = t;
}
return forcebundle;
}
/*** ************************ ***/
return forcebundle;
}
})();