Firefox for D3强制布局的性能下降

时间:2015-04-17 17:10:39

标签: javascript performance firefox d3.js

我使用D3创建了一个力布局(见下图)。但是,它在Firefox中的运行速度非常慢,而在Chrome中运行得非常好。我正在使用本地服务器进行调试并在http://localhost:8888/进行浏览。这可能是由于Firefox控制台中的以下消息,但相应的评论不太可能。有人可以查明性能问题,并给我一个如何解决它的提示吗?

mutating the [[Prototype]] of an object will cause your code to run very slowly; instead create the object with the correct initial [[Prototype]] value using Object.create

zip中的数据和代码:https://www.dropbox.com/s/ksh2qk1b5s9lfq5/Network%20View.zip?dl=0

可视化:

enter image description here

的index.html

<!DOCTYPE html>

<meta charset="utf-8">
<style>

.legend {                                                   
         font-size: 10px;                                         
      }                                                           
rect {                                                      
stroke-width: 2;                                          
}          

.node circle {
  stroke: white;
  stroke-width: 2px;
  opacity: 1.0;
}

line {
  stroke-width: 4px;
  stroke-opacity: 1.0;
  //stroke: "black"; 
}

body {
  /* Scaling for different browsers */
  -ms-transform: scale(1,1);
  -webkit-transform: scale(1,1);
  transform: scale(1,1);
}

svg{
    position:absolute;
    top:50%;
    left:0px;
}

</style>
<body>
<script type="text/javascript" src="d3.js"></script>
<script type="text/javascript" src="papaparse.js"></script> 
<script type="text/javascript" src="jquery.js"></script> 
<script type="text/javascript" src="networkview.js"></script>
</body>

networkview.js

var line_diff = 0.5;  // increase from zero if you want space between the call/text lines
var mark_offset = 10; // how many percent of the mark lines in each end are not used for the relationship between incoming/outgoing?
var mark_size = 5;    // size of the mark on the line

var legendRectSize = 9; // 18
var legendSpacing = 4; // 4
var recordTypes = [];
var legend;

var text_links_data, call_links_data;

// colors for the different parts of the visualization
recordTypes.push({
    text : "call",
    color : "#438DCA"
});

recordTypes.push({
    text : "text",
    color : "#70C05A"
});

recordTypes.push({
    text : "balance",
    color : "#245A76"
});

// Function for grabbing a specific property from an array
pluck = function (ary, prop) {
    return ary.map(function (x) {
        return x[prop]
    });
}

// Sums an array
sum = function (ary) {
    return ary.reduce(function (a, b) {
        return a + b
    }, 0);
}

maxArray = function (ary) {
        return ary.reduce(function (a, b) {
            return Math.max(a, b)
        }, -Infinity);
    }

minArray = function (ary) {
    return ary.reduce(function (a, b) {
        return Math.min(a, b)
    }, Infinity);
}

var data_links;
var data_nodes;

var results = Papa.parse("links.csv", {
        header : true,
        download : true,
        dynamicTyping : true,
        delimiter : ",",
        skipEmptyLines : true,
        complete : function (results) {
            data_links = results.data;
            dataLoaded();
        }
    });

var results = Papa.parse("nodes.csv", {
        header : true,
        download : true,
        dynamicTyping : true,
        delimiter : ",",
        skipEmptyLines : true,
        complete : function (results) {
            data_nodes = results.data;
            data_nodes.forEach(function (d, i) {
                d.size = (i == 0)? 200 : 30
                d.fill = (d.no_network_info == 1)? "#dfdfdf": "#a8a8a8"
            });
            dataLoaded();
        }
    });

function node_radius(d) {
    return Math.pow(40.0 * ((d.index == 0) ? 200 : 30), 1 / 3);
}
function node_radius_data(d) {
    return Math.pow(40.0 * d.size, 1 / 3);
}

function dataLoaded() {
    if (typeof data_nodes === "undefined" || typeof data_links === "undefined") {
        //console.log("Still loading")
    } else {
        CreateVisualizationFromData();
    }
}

function isConnectedToOtherThanMain(a) {
    var connected = false;
    for (i = 1; i < data_nodes.length; i++) {
        if (isConnected(a, data_nodes[i]) && a.index != i) {
            connected = true;
        }
    }
    return connected;
}

function isConnected(a, b) {
    return isConnectedAsTarget(a, b) || isConnectedAsSource(a, b) || a.index == b.index;
}

function isConnectedAsSource(a, b) {
    return linkedByIndex[a.index + "," + b.index];
}

function isConnectedAsTarget(a, b) {
    return linkedByIndex[b.index + "," + a.index];
}

function isEqual(a, b) {
    return a.index == b.index;
}

function tick() {

    if (call_links_data.length > 0) {
        callLink
        .attr("x1", function (d) {
            return d.source.x - line_perpendicular_shift(d, 1)[0] + line_radius_shift_to_edge(d, 0)[0];
        })
        .attr("y1", function (d) {
            return d.source.y - line_perpendicular_shift(d, 1)[1] + line_radius_shift_to_edge(d, 0)[1];
        })
        .attr("x2", function (d) {
            return d.target.x - line_perpendicular_shift(d, 1)[0] + line_radius_shift_to_edge(d, 1)[0];
        })
        .attr("y2", function (d) {
            return d.target.y - line_perpendicular_shift(d, 1)[1] + line_radius_shift_to_edge(d, 1)[1];
        });
        callLink.each(function (d) {
            applyGradient(this, "call", d)
        });
    }

    if (text_links_data.length > 0) {
        textLink
        .attr("x1", function (d) {
            return d.source.x - line_perpendicular_shift(d, -1)[0] + line_radius_shift_to_edge(d, 0)[0];
        })
        .attr("y1", function (d) {
            return d.source.y - line_perpendicular_shift(d, -1)[1] + line_radius_shift_to_edge(d, 0)[1];
        })
        .attr("x2", function (d) {
            return d.target.x - line_perpendicular_shift(d, -1)[0] + line_radius_shift_to_edge(d, 1)[0];
        })
        .attr("y2", function (d) {
            return d.target.y - line_perpendicular_shift(d, -1)[1] + line_radius_shift_to_edge(d, 1)[1];
        });
        textLink.each(function (d) {
            applyGradient(this, "text", d)
        });

        node
        .attr("transform", function (d) {
            return "translate(" + d.x + "," + d.y + ")";
        });
    }



    if (force.alpha() < 0.05)
        drawLegend();
}

function getRandomInt() {
    return Math.floor(Math.random() * (100000 - 0));
}

function applyGradient(line, interaction_type, d) {
    var self = d3.select(line);

    var current_gradient = self.style("stroke")
    //current_gradient = current_gradient.substring(4, current_gradient.length - 1);



    if (current_gradient.match("http")) {
        var parts = current_gradient.split("/");
        current_gradient = parts[-1];
    } else {
        current_gradient = current_gradient.substring(4, current_gradient.length - 1);
    }

    var new_gradient_id = "line-gradient" + getRandomInt();

    var from = d.source.size < d.target.size ? d.source : d.target;
    var to = d.source.size < d.target.size ? d.target : d.source;

    var mid_offset = 0;
    var standardColor = "";

    if (interaction_type == "call") {
        mid_offset = d.inc_calls / (d.inc_calls + d.out_calls);
        standardColor = "#438DCA";
    } else {
        mid_offset = d.inc_texts / (d.inc_texts + d.out_texts);
        standardColor = "#70C05A";
    }

    /* recordTypes_ID = pluck(recordTypes, 'text');
    whichRecordType = recordTypes_ID.indexOf(interaction_type);
    standardColor = recordTypes[whichRecordType].color;
 */
    mid_offset = mid_offset * 100;
    mid_offset = mid_offset * 0.6 + 20; // scale so it doesn't hit the ends

    lineLengthCalculation = function (x, y, x0, y0) {
        return Math.sqrt((x -= x0) * x + (y -= y0) * y);
    };

    lineLength = lineLengthCalculation(from.px, from.py, to.px, to.py);

    if (lineLength >= 0.1) {
        mark_size_percent = (mark_size / lineLength) * 100;

        defs.append("linearGradient")
        .attr("id", new_gradient_id)
        .attr("gradientUnits", "userSpaceOnUse")
        .attr("x1", from.px)
        .attr("y1", from.py)
        .attr("x2", to.px)
        .attr("y2", to.py)
        .selectAll("stop")
        .data([{
                    offset : "0%",
                    color : standardColor,
                    opacity : "1"
                }, {
                    offset : Math.round(mid_offset - mark_size_percent / 2) + "%",
                    color : standardColor,
                    opacity : "1"
                }, {
                    offset : Math.round(mid_offset - mark_size_percent / 2) + "%",
                    color : standardColor,
                    opacity : "1"
                }, {
                    offset : Math.round(mid_offset - mark_size_percent / 2) + "%",
                    color : "#245A76",
                    opacity : "1"
                }, {
                    offset : Math.round(mid_offset + mark_size_percent / 2) + "%",
                    color : "#245A76",
                    opacity : "1"
                }, {
                    offset : Math.round(mid_offset + mark_size_percent / 2) + "%",
                    color : standardColor,
                    opacity : "1"
                }, {
                    offset : Math.round(mid_offset + mark_size_percent / 2) + "%",
                    color : standardColor,
                    opacity : "1"
                }, {
                    offset : "100%",
                    color : standardColor,
                    opacity : "1"
                }
            ])
        .enter().append("stop")

        .attr("offset", function (d) {
            return d.offset;
        })
        .attr("stop-color", function (d) {
            return d.color;
        })
        .attr("stop-opacity", function (d) {
            return d.opacity;
        });

        self.style("stroke", "url(#" + new_gradient_id + ")")

        defs.select(current_gradient).remove();
    }
}

var linkedByIndex;

var width = $(window).width();
var height = $(window).height();

var svg = d3.select("body").append("svg")
    .attr("width", width)
    .attr("height", height);

var force;
var callLink;
var textLink;
var link;
var node;
var defs;
var total_interactions = 0;
var max_interactions = 0;

function CreateVisualizationFromData() {

    for (i = 0; i < data_links.length; i++) {
        total_interactions += data_links[i].inc_calls + data_links[i].out_calls + data_links[i].inc_texts + data_links[i].out_texts;
        max_interactions = Math.max(max_interactions, data_links[i].inc_calls + data_links[i].out_calls + data_links[i].inc_texts + data_links[i].out_texts)
    }

    linkedByIndex = {};

    data_links.forEach(function (d) {
        linkedByIndex[d.source + "," + d.target] = true;
        //linkedByIndex[d.source.index + "," + d.target.index] = true;
    });

    //console.log(total_interactions);
    //console.log(max_interactions);

    function chargeForNode(d, i) {
        // main node
        if (i == 0) {
            return -25000;
        }
        // contains other links
        else if (isConnectedToOtherThanMain(d)) {
            return -2000;
        } else {
            return -1200;
        }
    }

    // initial placement of nodes prevents overlaps
    central_x = width / 2
    central_y = height / 2

    data_nodes.forEach(function(d, i) {
    if (i != 0) {
            connected = isConnectedToOtherThanMain(d);
            data_nodes[i].x = connected? central_x + 10000: central_x -10000;
            data_nodes[i].y = connected? central_y: central_y;
    }
    else {data_nodes[i].x = central_x; data_nodes[i].y = central_y;}})

    force = d3.layout.force()
        .nodes(data_nodes)
        .links(data_links)
        .charge(function (d, i) {
            return chargeForNode(d, i)
        })
        .friction(0.6) // 0.6
        .gravity(0.4) // 0.6
        .size([width, height])
        .start();

    call_links_data = data_links.filter(function(d) {
        return (d.inc_calls + d.out_calls > 0)});
    text_links_data = data_links.filter(function(d) {
        return (d.inc_texts + d.out_texts > 0)});

    callLink = svg.selectAll(".call-line")
        .data(call_links_data)
        .enter().append("line");
    textLink = svg.selectAll(".text-line")
        .data(text_links_data)
        .enter().append("line");
    link = svg.selectAll("line");

    node = svg.selectAll(".node")
        .data(data_nodes)
        .enter().append("g")
        .attr("class", "node");


    defs = svg.append("defs");

    node
    .append("circle")
    .attr("r", node_radius)
    .style("fill", function (d) {
        return (d.index == 0)? "#ffffff" : d.fill;
    })
    .style("stroke", function (d) {
        return (d.index == 0)? "#8C8C8C" : "#ffffff";
    })

    svg
    .append("marker")
    .attr("id", "arrowhead")
    .attr("refX", 6 + 7)
    .attr("refY", 2)
    .attr("markerWidth", 6)
    .attr("markerHeight", 4)
    .attr("orient", "auto")
    .append("path")
    .attr("d", "M 0,0 V 4 L6,2 Z");

    if (text_links_data.length > 0) {
        textLink
        .style("stroke-width", function stroke(d) {
            return text_width(d)
        })
        .each(function (d) {
            applyGradient(this, "text", d)
        });
    }

    if (call_links_data.length > 0) {
        callLink
        .style("stroke-width", function stroke(d) {
            return call_width(d)
        })
        .each(function (d) {
            applyGradient(this, "call", d)
        });
    }

    force
    .on("tick", tick);

}

function drawLegend() {

    var node_px = pluck(data_nodes, 'px');
    var node_py = pluck(data_nodes, 'py');
    var nodeLayoutRight  = Math.max(maxArray(node_px));
    var nodeLayoutBottom = Math.max(maxArray(node_py));

    legend = svg.selectAll('.legend')
        .data(recordTypes)
        .enter()
        .append('g')
        .attr('class', 'legend')
        .attr('transform', function (d, i) {
            var rect_height = legendRectSize + legendSpacing;
            var offset = rect_height * (recordTypes.length-1);
            var horz = nodeLayoutRight + 15; /*  - 2*legendRectSize; */
            var vert = nodeLayoutBottom + (i * rect_height) - offset;
            return 'translate(' + horz + ',' + vert + ')';
        });

    legend.append('rect')
    .attr('width', legendRectSize)
    .attr('height', legendRectSize)
    .style('fill', function (d) {
        return d.color
    })
    .style('stroke', function (d) {
        return d.color
    });

    legend.append('text')
    .attr('x', legendRectSize + legendSpacing)
    .attr('y', legendRectSize - legendSpacing + 3)
    .text(function (d) {
        return d.text;
    })
    .style('fill', '#757575');

}

var line_width_factor = 10.0 // width for the widest line

function call_width(d) {
    return (d.inc_calls + d.out_calls) / max_interactions * line_width_factor;
}

function text_width(d) {
    return (d.inc_texts + d.out_texts) / max_interactions * line_width_factor;
}

function total_width(d) {
    return (d.inc_calls + d.out_calls + d.inc_texts + d.out_texts) / max_interactions * line_width_factor + line_diff;
}

function line_perpendicular_shift(d, direction) {
    theta = getAngle(d);
    theta_perpendicular = theta + (Math.PI / 2) * direction;

    lineWidthOfOppositeLine = direction == 1 ? text_width(d) : call_width(d);
    shift = lineWidthOfOppositeLine / 2;

    delta_x = (shift + line_diff) * Math.cos(theta_perpendicular)
    delta_y = (shift + line_diff) * Math.sin(theta_perpendicular)

    return [delta_x, delta_y]

}

function line_radius_shift_to_edge(d, which_node) { // which_node = 0 if source, = 1 if target

    theta = getAngle(d);
    theta = (which_node == 0) ? theta : theta + Math.PI; // reverse angle if target node
    radius = (which_node == 0) ? node_radius(d.source) : node_radius(d.target) // d.source and d.target refer directly to the nodes (not indices)
    radius -= 2; // add stroke width

    delta_x = radius * Math.cos(theta)
        delta_y = radius * Math.sin(theta)

        return [delta_x, delta_y]

}

function getAngle(d) {
    rel_x = d.target.x - d.source.x;
    rel_y = d.target.y - d.source.y;
    return theta = Math.atan2(rel_y, rel_x);
}

Links.csv

source,target,inc_calls,out_calls,inc_texts,out_texts
0,1,1.0,0.0,1.0,0.0
0,2,0.0,0.0,1.0,3.0
0,3,3.0,9.0,5.0,7.0
0,4,2.0,12.0,9.0,14.0
0,5,5.0,9.0,9.0,13.0
0,6,5.0,17.0,2.0,25.0
0,7,6.0,13.0,7.0,16.0
0,8,7.0,7.0,8.0,8.0
0,9,3.0,10.0,8.0,20.0
0,10,5.0,10.0,6.0,23.0
0,11,8.0,10.0,13.0,15.0
0,12,9.0,18.0,9.0,22.0
0,13,1.0,2.0,2.0,2.0
0,14,11.0,13.0,7.0,15.0
0,15,5.0,18.0,9.0,22.0
0,16,8.0,15.0,13.0,20.0
0,17,4.0,10.0,9.0,26.0
0,18,9.0,18.0,8.0,33.0
0,19,12.0,11.0,4.0,15.0
0,20,4.0,15.0,9.0,25.0
0,21,4.0,17.0,10.0,19.0
0,22,4.0,16.0,12.0,29.0
0,23,6.0,9.0,12.0,20.0
0,24,2.0,2.0,1.0,3.0
0,25,3.0,8.0,10.0,16.0
0,26,3.0,10.0,11.0,22.0
0,27,6.0,14.0,9.0,11.0
0,28,2.0,7.0,8.0,15.0
0,29,2.0,11.0,8.0,15.0
0,30,1.0,8.0,9.0,6.0
0,31,3.0,6.0,7.0,7.0
0,32,4.0,9.0,3.0,12.0
0,33,4.0,4.0,7.0,12.0
0,34,4.0,4.0,5.0,9.0
0,35,2.0,3.0,0.0,7.0
0,36,3.0,7.0,5.0,9.0
0,37,1.0,7.0,5.0,3.0
0,38,1.0,13.0,1.0,2.0
0,39,2.0,7.0,3.0,4.0
0,40,1.0,3.0,2.0,6.0
0,41,0.0,1.0,2.0,1.0
0,42,0.0,0.0,2.0,0.0
0,43,0.0,3.0,1.0,5.0
0,44,0.0,1.0,0.0,2.0
0,45,4.0,1.0,1.0,10.0
0,46,2.0,7.0,3.0,5.0
0,47,5.0,7.0,3.0,5.0
0,48,2.0,5.0,4.0,10.0
0,49,3.0,3.0,5.0,13.0
1,15,10.0,30.0,13.0,37.0
2,8,16.0,9.0,24.0,15.0
2,43,4.0,10.0,9.0,16.0
5,48,3.0,5.0,0.0,4.0
6,37,11.0,25.0,15.0,34.0
8,48,12.0,4.0,7.0,2.0
9,42,25.0,9.0,29.0,15.0
9,45,11.0,3.0,16.0,5.0
12,24,4.0,15.0,13.0,16.0
14,31,18.0,9.0,29.0,12.0
14,33,5.0,10.0,4.0,9.0
15,28,8.0,5.0,16.0,5.0
16,36,14.0,11.0,10.0,19.0
23,38,3.0,11.0,6.0,10.0
26,42,9.0,23.0,17.0,21.0
27,46,12.0,12.0,15.0,21.0
29,39,8.0,15.0,9.0,20.0
29,47,8.0,27.0,19.0,24.0
33,46,6.0,4.0,13.0,13.0
37,43,10.0,12.0,6.0,21.0

Nodes.csv

no_network_info
0
0
0
1
1
0
0
0
0
0
0
1
0
1
0
0
0
1
0
1
1
0
0
0
0
1
0
0
0
0
1
0
1
0
1
1
0
0
0
0
1
1
0
0
1
0
0
0
0
0

1 个答案:

答案 0 :(得分:1)

  

修改
  问题的根本原因是由于未能删除linearGradient部分中过时的defs标记导致文档膨胀   HTML。这只发生在Firefox中,因为它返回了什么   响应getPropertyValue中的CSSStyleDeclaration selection.style()   interface(由"url("http://localhost:88888/index.html#line-gradientXXXXXX") transparent"中的d3调用)。价值   返回的是表单   "url(#line-gradientXXXXXX)",与另一个id相比   浏览器。由于OP未正确提取linearGradient,   未找到标记为删除的linearGradient标记   删除,导致它们数量增长。这个问题可以避免   使用数据中已有的唯一索引来标记   forEach代码。

根据我上面的评论,我设法通过进行以下更改来解决Firefox问题:

  1. 消除tickapplyGradientd3部分的冗余计算。
  2. 使用格式正确的defs来管理d3。这可能很好,它只是花了我一段时间才意识到它是如何完成的,但是,我把它改为标准var new_gradient_id = "line-gradient" + getRandomInt();模式,这将管理更新和正确更改数据。这条线特别敏感...
    var new_gradient_id = "lg" + interaction_type + d.source.index + d.target.index;
    这样做效果更好......
    callLink
  3. 应用标准d3模式来管理textLink中的CreateVisualizationFromDatalinearGradient部分。使用这些模式,它可以正确更新并管理不断变化的数据。
  4. 在进行这些更改后,Firefox中的速度问题消失了,现在在速度方面在所有三个主要浏览器中都是相同的。但它在Chrome中看起来更好。一些实验将是为了确切地确定哪些更改是关键的,但是删除<!DOCTYPE html> <meta charset="utf-8"> <style> /*div { outline: 1px solid black;*/ } .legend { font-size: 10px; } rect { stroke-width: 2; } .node circle { stroke: white; stroke-width: 2px; opacity: 1.0; } line { stroke-width: 4px; stroke-opacity: 1.0; //stroke: "black"; } body { /* Scaling for different browsers */ -ms-transform: scale(1,1); -webkit-transform: scale(1,1); transform: scale(1,1); } svg{ position:absolute; top:50%; left:0px; } </style> <body> <script src="http://d3js.org/d3.v3.min.js"></script> <div style="margin: 50px 0 10px 50px; display: inline-block">click to start/stop</div> <!--<script src="d3/d3 CB.js"></script>--> <script type="text/javascript" src="jquery.js"></script> <script type="text/javascript" src="papaparse.js"></script> <script type="text/javascript" src="networkview CB.js"></script> </body> 标签肯定存在问题。这些没有在FF中被正确删除并且大量膨胀DOM。我认为这可能是导致问题的原因。

    我所做的其他改变只是风格化,让我更容易理解。

    修改后的代码:
    HTML

    var line_diff = 0.5;  // increase from zero if you want space between the call/text lines
    var mark_offset = 10; // how many percent of the mark lines in each end are not used for the relationship between incoming/outgoing?
    var mark_size = 5;    // size of the mark on the line
    
    var legendRectSize = 9; // 18
    var legendSpacing = 4; // 4
    var recordTypes = [];
    var legend;
    
    var text_links_data, call_links_data;
    
    // colors for the different parts of the visualization
    recordTypes.push({
        text : "call",
        color : "#438DCA"
    });
    
    recordTypes.push({
        text : "text",
        color : "#70C05A"
    });
    
    recordTypes.push({
        text : "balance",
        color : "#245A76"
    });
    
    // Function for grabbing a specific property from an array
    pluck = function (ary, prop) {
        return ary.map(function (x) {
            return x[prop]
        });
    }
    
    // Sums an array
    sum = function (ary) {
        return ary.reduce(function (a, b) {
            return a + b
        }, 0);
    }
    
    maxArray = function (ary) {
            return ary.reduce(function (a, b) {
                return Math.max(a, b)
            }, -Infinity);
        }
    
    minArray = function (ary) {
        return ary.reduce(function (a, b) {
            return Math.min(a, b)
        }, Infinity);
    }
    
    var data_links;
    
    var data_nodes;
    
    var results = Papa.parse("links.csv", {
            header : true,
            download : true,
            dynamicTyping : true,
            delimiter : ",",
            skipEmptyLines : true,
            complete : function (results) {
                data_links = results.data;
    
                for (i = 0; i < data_links.length; i++) {
                    total_interactions += data_links[i].inc_calls
                                                                + data_links[i].out_calls
                                                                + data_links[i].inc_texts
                                                                + data_links[i].out_texts;
                    max_interactions = Math.max(max_interactions,
                                                                            data_links[i].inc_calls
                                                                            + data_links[i].out_calls
                                                                            + data_links[i].inc_texts
                                                                            + data_links[i].out_texts)
                }
    
                //console.log(total_interactions);
                //console.log(max_interactions);
    
                linkedByIndex = {};
    
                data_links.forEach(function (d) {
                    linkedByIndex[d.source + "," + d.target] = true;
                    //linkedByIndex[d.source.index + "," + d.target.index] = true;
                });
    
                dataLoaded();
            }
        });
    
    var results = Papa.parse("nodes.csv", {
            header : true,
            download : true,
            dynamicTyping : true,
            delimiter : ",",
            skipEmptyLines : true,
            complete : function (results) {
                data_nodes = results.data;
                data_nodes.forEach(function (d, i) {
                    d.size = (i == 0)? 200 : 30
                    d.fill = (d.no_network_info == 1)? "#dfdfdf": "#a8a8a8"
                });
                dataLoaded();
            }
        });
    
    function node_radius(d) {
        return Math.pow(40.0 * ((d.index == 0) ? 200 : 30), 1 / 3);
    }
    function node_radius_data(d) {
        return Math.pow(40.0 * d.size, 1 / 3);
    }
    
    function dataLoaded() {
        if (typeof data_nodes === "undefined" || typeof data_links === "undefined") {
            console.log("Still loading " + (typeof data_nodes === "undefined" ? 'data_links' : 'data_nodes'))
        } else {
            CreateVisualizationFromData();
        }
    }
    
    function isConnectedToOtherThanMain(a) {
        var connected = false;
        for (i = 1; i < data_nodes.length; i++) {
            if (isConnected(a, data_nodes[i]) && a.index != i) {
                connected = true;
            }
        }
        return connected;
    }
    
    function isConnected(a, b) {
        return isConnectedAsTarget(a, b) || isConnectedAsSource(a, b) || a.index == b.index;
    }
    
    function isConnectedAsSource(a, b) {
        return linkedByIndex[a.index + "," + b.index];
    }
    
    function isConnectedAsTarget(a, b) {
        return linkedByIndex[b.index + "," + a.index];
    }
    
    function isEqual(a, b) {
        return a.index == b.index;
    }
    
    var log = d3.select('body').append('div').attr('id', 'log').style({margin: '50px 0 10px 3px', display: 'inline-block'});
    log.update = function (alpha) {
        this.text('alpha: ' + d3.format(".3f")(alpha))
    }
    
    function tick(e) {
    
        log.update(e.alpha)
    
            if (call_links_data.length > 0) {
    
            callLink
            //CB eliminate redundant calculations
            .each(function (d) {
                d.lpf1 = line_perpendicular_shift(d, 1)
                d.lrste = []
                d.lrste.push(line_radius_shift_to_edge(d, 0))
                d.lrste.push(line_radius_shift_to_edge(d, 1))
            })
            //CB
            .attr("x1", function (d) {
                return d.source.x - d.lpf1[0] + d.lrste[0][0];
            })
            .attr("y1", function (d) {
                return d.source.y - d.lpf1[1] + d.lrste[0][1];
            })
            .attr("x2", function (d) {
                return d.target.x - d.lpf1[0] + d.lrste[1][0];
            })
            .attr("y2", function (d) {
                return d.target.y - d.lpf1[1] + d.lrste[1][1];
            });
            callLink.each(function (d, i) {
                applyGradient(this, "call", d, i)
            });
    
                }
    
        if (text_links_data.length > 0) {
    
                    textLink
            //CB
            .each(function (d) {
                d.lpfNeg1 = line_perpendicular_shift(d, -1);
                d.lrste = [];
                d.lrste.push(line_radius_shift_to_edge(d, 0));
                d.lrste.push(line_radius_shift_to_edge(d, 1));
            })
            //CB
            .attr("x1", function (d) {
                return d.source.x - d.lpfNeg1[0] + d.lrste[0][0];
            })
            .attr("y1", function (d) {
                return d.source.y - d.lpfNeg1[1] + d.lrste[0][1];
            })
            .attr("x2", function (d) {
                return d.target.x - d.lpfNeg1[0] + d.lrste[1][0];
            })
            .attr("y2", function (d) {
                return d.target.y - d.lpfNeg1[1] + d.lrste[1][1];
            });
            textLink.each(function (d, i) {
                applyGradient(this, "text", d, i)
            });
    
            node
            .attr("transform", function (d) {
                return "translate(" + d.x + "," + d.y + ")";
            });
    
                }
    
        if (force.alpha() < 0.05)
            drawLegend();
    
        }
    
    function getRandomInt() {
        return Math.floor(Math.random() * (100000 - 0));
    }
    
    function applyGradient(line, interaction_type, d, i) {
    
            var self = d3.select(line);
    
        var current_gradient = self.style("stroke");
            //current_gradient = current_gradient.substring(4, current_gradient.length - 1);
    
        if (current_gradient.match("http")) {
            var parts = current_gradient.split("/");
            current_gradient = parts[-1];
        } else {
            current_gradient = current_gradient.substring(4, current_gradient.length - 1);
        }
    
        var new_gradient_id = "lg" + interaction_type + d.source.index + d.target.index; // + getRandomInt();
    
        var from = d.source.size < d.target.size ? d.source : d.target;
        var to = d.source.size < d.target.size ? d.target : d.source;
    
        var mid_offset = 0;
        var standardColor = "";
    
        if (interaction_type == "call") {
            mid_offset = d.inc_calls / (d.inc_calls + d.out_calls);
            standardColor = "#438DCA";
        } else {
            mid_offset = d.inc_texts / (d.inc_texts + d.out_texts);
            standardColor = "#70C05A";
        }
    
        /* recordTypes_ID = pluck(recordTypes, 'text');
        whichRecordType = recordTypes_ID.indexOf(interaction_type);
        standardColor = recordTypes[whichRecordType].color;
     */
        mid_offset = mid_offset * 100;
        mid_offset = mid_offset * 0.6 + 20; // scale so it doesn't hit the ends
    
        lineLengthCalculation = function (x, y, x0, y0) {
            return Math.sqrt((x -= x0) * x + (y -= y0) * y);
        };
    
        lineLength = lineLengthCalculation(from.px, from.py, to.px, to.py);
    
        if (lineLength >= 0.1) {
            var mark_size_percent = (mark_size / lineLength) * 100,
                    _offsetDiff = Math.round(mid_offset - mark_size_percent / 2) + "%",
                    _offsetSum = Math.round(mid_offset + mark_size_percent / 2) + "%",
    
                defsUpdate = defs.selectAll("#" + new_gradient_id)
                .data([{
                    x1: from.px,
                    y1: from.py,
                    x2: to.px,
                    y2: to.py
            }]),
    
                defsEnter = defsUpdate.enter().append("linearGradient")
                    .attr("id", new_gradient_id)
                    .attr("gradientUnits", "userSpaceOnUse"),
    
                defsUpdateEnter = defsUpdate
                    .attr("x1", function (d) { return d.x1 })
                    .attr("y1", function (d) { return d.y1 })
                    .attr("x2", function (d) { return d.x2 })
                    .attr("y2", function (d) { return d.y2 }),
    
                stopsUpdate = defsUpdateEnter.selectAll("stop")
                    .data([{
                        offset: "0%",
                        color: standardColor,
                        opacity: "1"
                    }, {
                        offset: _offsetDiff,
                        color: standardColor,
                        opacity: "1"
                    }, {
                        offset: _offsetDiff,
                        color: standardColor,
                        opacity: "1"
                    }, {
                        offset: _offsetDiff,
                        color: "#245A76",
                        opacity: "1"
                    }, {
                        offset: _offsetSum,
                        color: "#245A76",
                        opacity: "1"
                    }, {
                        offset: _offsetSum,
                        color: standardColor,
                        opacity: "1"
                    }, {
                        offset: _offsetSum,
                        color: standardColor,
                        opacity: "1"
                    }, {
                        offset: "100%",
                        color: standardColor,
                        opacity: "1"
                    }
                    ]),
    
                    stopsEnter = stopsUpdate.enter().append("stop")
    
                stopsUpdateEnter = stopsUpdate
                .attr("offset", function (d) {
                    return d.offset;
                })
                .attr("stop-color", function (d) {
                    return d.color;
                })
                .attr("stop-opacity", function (d) {
                    return d.opacity;
                })
    
            self.style("stroke", "url(#" + new_gradient_id + ")")
    
            //current_gradient && defs.select(current_gradient).remove();   /*CB Edit*/
        }
    
        } /*applyGradient*/
    
    var linkedByIndex;
    
    var width = $(window).width();
    var height = $(window).height();
    
    var svg = d3.select("body").append("svg")
        .attr("width", width)
        .attr("height", height);
    
    var force;
    var callLink;
    var textLink;
    var link;
    var node;
    var defs;
    var marker;
    var total_interactions = 0;
    var max_interactions = 0;
    
    function CreateVisualizationFromData() {
    
        function chargeForNode(d, i) {
            // main node
            if (i == 0) {
                return -25000;
            }
                // contains other links
            else if (isConnectedToOtherThanMain(d)) {
                return -2000;
            } else {
                return -1200;
            }
        }
    
        // initial placement of nodes prevents overlaps
        var xOffset = 10000,
                yOffset = -10000,
                central_x = width / 2,
                central_y = height / 2;
    
        data_nodes.forEach(function(d, i) {
            if (i != 0) {
                connected = isConnectedToOtherThanMain(d);
                data_nodes[i].x = connected ? central_x + xOffset : central_x - xOffset;
                data_nodes[i].y = connected ? central_y + yOffset : central_y - yOffset;
            }
            else {data_nodes[i].x = central_x; data_nodes[i].y = central_y;}})
    
        force = d3.layout.force()
            .nodes(data_nodes)
            .links(data_links)
            .charge(function (d, i) {
                return chargeForNode(d, i)
            })
            .friction(0.6) // 0.6
            .gravity(0.4) // 0.6
            .size([width, height])
            .start()    //initialise alpha
            .stop();
    
        log.update(force.alpha());
    
        call_links_data = data_links.filter(function(d) {
            return (d.inc_calls + d.out_calls > 0)});
        text_links_data = data_links.filter(function(d) {
            return (d.inc_texts + d.out_texts > 0)});
    
        //UPDATE
        callLink = svg.selectAll(".call-line")
            .data(call_links_data)
        //ENTER
        callLink.enter().append("line")
            .attr('class', 'call-line');
        //EXIT
        callLink.exit().remove;
    
        //UPDATE
        textLink = svg.selectAll(".text-line")
            .data(text_links_data)
        //ENTER
        textLink.enter().append("line")
            .attr('class', 'text-line');
        //EXIT
        textLink.exit().remove;
    
        //UPDATE
        node = svg.selectAll(".node")
            .data(data_nodes)
            //CB the g elements are not needed because there is only one element
            //in each node...
        //ENTER
        node.enter().append("g")
            .attr("class", "node")
            .append("circle")
                .attr("r", node_radius)
                .style("fill", function (d) {
                    return (d.index == 0) ? "#ffffff" : d.fill;
                })
                .style("stroke", function (d) {
                    return (d.index == 0) ? "#8C8C8C" : "#ffffff";
                });
    
        //EXIT
        node.exit().remove;
    
        defs = !(defs && defs.length) ? svg.append("defs") : defs;
    
        marker = svg.selectAll('marker')
            .data([{refX: 6+7, refY: 2, markerWidth: 6, markerHeight: 4}])
        .enter().append("marker")
            .attr("id", "arrowhead")
            .attr("refX", function (d) { return d.refX })
            .attr("refY", function (d) { return d.refY })
            .attr("markerWidth", function (d) { return d.markerWidth })
            .attr("markerHeight", function (d) { return d.markerHeight })
            .attr("orient", "auto")
            .append("path")
                .attr("d", "M 0,0 V 4 L6,2 Z");
    
        if (text_links_data.length > 0) {
            //UPDATE + ENTER
            textLink
            .style("stroke-width", function stroke(d) {
                return text_width(d)
            })
            .each(function (d, i) {
                applyGradient(this, "text", d, i)
            });
        }
    
        if (call_links_data.length > 0) {
            //UPDATE + ENTER
            callLink
            .style("stroke-width", function stroke(d) {
                return call_width(d)
            })
            .each(function (d, i) {
                applyGradient(this, "call", d, i)
            });
        }
    
        force
        .on("tick", tick);
    
    
    }
    
    d3.select(document).on('click', (function () {
        var _disp = d3.dispatch('stop_start')
        return function (e) {
    
            if (!_disp.on('stop_start') || _disp.on('stop_start') === force.stop) {
                if (!_disp.on('stop_start')) {
                    _disp.on('stop_start', force.start)
                } else {
                    _disp.on('stop_start', function () {
                        CreateVisualizationFromData();
                        force.start()
                        //force.alpha(0.5)
                    })
                }
            } else {
                _disp.on('stop_start', force.stop)
            }
            _disp.stop_start()
        }
    })())
    
    function drawLegend() {
    
        var node_px = pluck(data_nodes, 'px');
        var node_py = pluck(data_nodes, 'py');
        var nodeLayoutRight  = Math.max(maxArray(node_px));
        var nodeLayoutBottom = Math.max(maxArray(node_py));
    
        legend = svg.selectAll('.legend')
            .data(recordTypes)
            .enter()
            .append('g')
            .attr('class', 'legend')
            .attr('transform', function (d, i) {
                var rect_height = legendRectSize + legendSpacing;
                var offset = rect_height * (recordTypes.length-1);
                var horz = nodeLayoutRight + 15; /*  - 2*legendRectSize; */
                var vert = nodeLayoutBottom + (i * rect_height) - offset;
                return 'translate(' + horz + ',' + vert + ')';
            });
    
        legend.append('rect')
        .attr('width', legendRectSize)
        .attr('height', legendRectSize)
        .style('fill', function (d) {
            return d.color
        })
        .style('stroke', function (d) {
            return d.color
        });
    
        legend.append('text')
        .attr('x', legendRectSize + legendSpacing)
        .attr('y', legendRectSize - legendSpacing + 3)
        .text(function (d) {
            return d.text;
        })
        .style('fill', '#757575');
    
    }
    
    var line_width_factor = 10.0 // width for the widest line
    
    function call_width(d) {
        return (d.inc_calls + d.out_calls) / max_interactions * line_width_factor;
    }
    
    function text_width(d) {
        return (d.inc_texts + d.out_texts) / max_interactions * line_width_factor;
    }
    
    function total_width(d) {
        return (d.inc_calls + d.out_calls + d.inc_texts + d.out_texts) / max_interactions * line_width_factor + line_diff;
    }
    
    function line_perpendicular_shift(d, direction) {
        theta = getAngle(d);
        theta_perpendicular = theta + (Math.PI / 2) * direction;
    
        lineWidthOfOppositeLine = direction == 1 ? text_width(d) : call_width(d);
        shift = lineWidthOfOppositeLine / 2;
    
        delta_x = (shift + line_diff) * Math.cos(theta_perpendicular)
        delta_y = (shift + line_diff) * Math.sin(theta_perpendicular)
    
        return [delta_x, delta_y]
    
    }
    
    function line_radius_shift_to_edge(d, which_node) { // which_node = 0 if source, = 1 if target
    
        theta = getAngle(d);
        theta = (which_node == 0) ? theta : theta + Math.PI; // reverse angle if target node
        radius = (which_node == 0) ? node_radius(d.source) : node_radius(d.target) // d.source and d.target refer directly to the nodes (not indices)
        radius -= 2; // add stroke width
    
        delta_x = radius * Math.cos(theta)
            delta_y = radius * Math.sin(theta)
    
            return [delta_x, delta_y]
    
    }
    
    function getAngle(d) {
        rel_x = d.target.x - d.source.x;
        rel_y = d.target.y - d.source.y;
        return theta = Math.atan2(rel_y, rel_x);
    }
    

    JS

    {{1}}