Reputation: 5923
I've created a force-layout using D3 (see image below). However, it runs very slowly in Firefox, whereas it works perfectly fine in Chrome. I'm debugging it using a local server and browsing at http://localhost:8888/. It's might be due to the following message in the Firefox console, but accordingly to the comments that's unlikely. Can someone pinpoint the performance issue and give me a hint on how to resolve it?
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
Data and code in zip: https://www.dropbox.com/s/ksh2qk1b5s9lfq5/Network%20View.zip?dl=0
Visualization:
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
Upvotes: 0
Views: 2210
Reputation: 6476
EDIT
The root cause of the problem was document bloat caused by failing to remove outdatedlinearGradient
tags in thedefs
section of the HTML. This was only happening in Firefox because of what it returns in response togetPropertyValue
in it'sCSSStyleDeclaration
interface (which is called by d3 inselection.style()
). The value returned is of the form"url("http://localhost:88888/index.html#line-gradientXXXXXX") transparent"
, compared to"url(#line-gradientXXXXXX)"
in the other browsers. Since theid
was not properly extracted by the OP,linearGradient
tags ear-marked for deletion were not found and not deleted, causing them to grow in number. The problem is avoided by using unique indexing, already available in the data, to label thelinearGradient
tags.
As per my comments above, I managed to solve the Firefox problem by making the following changes:
forEach
sections in tick
and applyGradient
.d3
to manage the defs
. It was probably fine how it was, it just took me a while to realise how it was done but, I changed it to standard d3
patterns which will manage updating and changing data properly. This line is particularly sensitive...var new_gradient_id = "line-gradient" + getRandomInt();
var new_gradient_id = "lg" + interaction_type + d.source.index + d.target.index;
callLink
and textLink
sections in CreateVisualizationFromData
. Using these patterns it updates properly and manages changing data.After making these changes, the speed problems in Firefox disapeared and it is now the same in all three major browsers in terms of speed. It looks better in Chrome though. Some experimenting would be in order to determine exactly which changes are critical, but there was definitely a problem with deleting the linearGradient
tags. These were not being properly deleted in FF and massively bloating the DOM. I think this is probably what was causing the problem.
The other changes I made were just stylistic to make it easier for me to understand.
Amended code:
HTML
<!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>
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;
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);
}
Upvotes: 1