Slow performance in Firefox for D3 force layout - javascript

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

EDIT
The root cause of the problem was document bloat caused by failing to remove outdated linearGradient tags in the defs section of the
HTML. This was only happening in Firefox because of what it returns
in response to getPropertyValue in it's CSSStyleDeclaration
interface (which is called by d3 in selection.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 the id 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 the
linearGradient tags.
As per my comments above, I managed to solve the Firefox problem by making the following changes:
Eliminate redundant calculations in the forEach sections in tick and applyGradient.
Using well-formed 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();
this works better...
var new_gradient_id = "lg" + interaction_type + d.source.index + d.target.index;
Applied standard d3 patterns to managing the 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);
}

Related

How to create a dashed line with arrows on each dash in d3?

How to make a line from arrows like a dashed line but somewhat like this "> > > >" for direction of the flow. I am trying to connect nodes via lines but my nodes have to resizable rectangles and it hides the arrowheads , which is why I am looking for such arrow(ed) line. Specifying arrow in the middle could also solve the problem but attr("marker-middle", "url(#arrowhead)") does not work in my code.
Here's the relevant code
svg
.append("defs")
.append("marker")
.attrs({
id: "arrowhead",
viewBox: "-0 -5 10 10",
refX: 13,
refY: 0,
orient: "auto",
markerWidth: 3,
markerHeight: 3
})
.append("svg:path")
.attr("d", "M 0,-5 L 10 ,0 L 0,5")
.attr("fill", "black")
.call(
d3.zoom().on("zoom", function() {
console.log(d3.event.transform);
svg.attr("transform", d3.event.transform);
})
);
var link = svg
.append("g")
.selectAll("line_class.line")
.data(graph.links)
.enter()
.append("line")
.attr("stroke-width", function(d) {
return Math.max(
((d.thickness - min_thickness) /
(max_thickness - min_thickness + 0.01)) *
5,
3
);
})
.style("stroke", "pink")
.text("text", function(d) {
return d.linkname;
})
.on("mouseover", function(d) {
div
.transition()
.duration(200)
.style("opacity", 0.9);
div
.html("Name: " + d.linkname)
.style("left", d3.event.pageX + "px")
.style("top", d3.event.pageY - 28 + "px")
.style("width", "auto")
.style("height", "auto");
})
.on("mouseout", function(d) {
div
.transition()
.duration(500)
.style("opacity", 0);
});
link.call(updateState1);
function updateState1() {
link
.each(function(d) {
//d3.select(this).attr("marker-mid", "url(#arrowhead)");
d3.select(this).attr("marker-end", "url(#arrowhead)");
});
}
var config = [];
var graph = {
nodes: [
{ name: "A" },
{ name: "B" },
{ name: "C" },
{ name: "D" },
{ name: "Dummy1" },
{ name: "Dummy2" },
{ name: "Dummy3" },
{ name: "Dummy4" }
],
links: [
{ source: "A", target: "B", linkname: "A0" },
{ source: "A", target: "C", linkname: "A0" },
{ source: "A", target: "D", linkname: "A1" },
{ source: "Dummy1", target: "A", linkname: "input" },
{ source: "B", target: "Dummy2", linkname: "B" },
{ source: "C", target: "Dummy3", linkname: "C" },
{ source: "D", target: "Dummy4", linkname: "D" }
]
};
var optArray = [];
labelAnchors = [];
labelAnchorLinks = [];
highlightNode_button = d3.select("#highlightNode");
highlightNode_button.on("click", highlightNode);
shrinkNode_button = d3.select("#shrinkNode");
shrinkNode_button.on("click", minimizeNode);
for (var i = 0; i < graph.nodes.length - 1; i++) {
optArray.push(graph.nodes[i].name);
}
optArray = optArray.sort();
$(function() {
$("#targetNode").autocomplete({
source: optArray
});
});
//used to find max thickness of all the nodes, which is used for normalizing later on.
var max_thickness = d3.max(graph.links, function(d) {
return d.thickness;
});
//used to find min thickness of all the nodes, which is used for normalizing later on.
var min_thickness = d3.min(graph.links, function(d) {
return d.thickness;
});
var svg1 = d3.select("svg");
var width = +screen.width;
var height = +screen.height - 500;
svg1.attr("width", width).attr("height", height);
var zoom = d3.zoom().on("zoom", zoomed);
function zoomed() {
svg.attr("transform", d3.event.transform);
}
var svg = svg1
// .call(
// zoom.on("zoom", function() {
// svg.attr("transform", d3.event.transform);
// })
// )
.call(zoom)
.on("dblclick.zoom", null)
.append("g");
// Defining the gradient
//used for coloring the nodes
var gradient = svg
.append("svg:defs")
.append("svg:linearGradient")
.attr("id", "gradient")
.attr("x1", "0%")
.attr("y1", "0%")
.attr("x2", "100%")
.attr("y2", "100%")
.attr("spreadMethod", "pad");
// Define the gradient colors
gradient
.append("svg:stop")
.attr("offset", "0%")
.attr("stop-color", "#b721ff")
.attr("stop-opacity", 1);
gradient
.append("svg:stop")
.attr("offset", "100%")
.attr("stop-color", "#21d4fd")
.attr("stop-opacity", 1);
var linkText = svg
.selectAll(".gLink")
.data(graph.links)
.append("text")
.attr("font-family", "Arial, Helvetica, sans-serif")
.attr("x", function(d) {
if (d.target.x > d.source.x) {
return d.source.x + (d.target.x - d.source.x) / 2;
} else {
return d.target.x + (d.source.x - d.target.x) / 2;
}
})
.attr("y", function(d) {
if (d.target.y > d.source.y) {
return d.source.y + (d.target.y - d.source.y) / 2;
} else {
return d.target.y + (d.source.y - d.target.y) / 2;
}
})
.attr("fill", "Black")
.style("font", "normal 12px Arial")
.attr("dy", ".35em")
.text(function(d) {
return d.linkname;
});
var dragDrop = d3
.drag()
.on("start", node => {
node.fx = node.x;
node.fy = node.y;
})
.on("drag", node => {
simulation.alphaTarget(1).restart();
node.fx = d3.event.x;
node.fy = d3.event.y;
})
.on("end", node => {
if (!d3.event.active) {
simulation.alphaTarget(0);
}
node.fx = null;
node.fy = null;
});
var linkForce = d3
.forceLink(graph.links)
.id(function(d) {
return d.name;
})
.distance(50);
//.strength(0.5) to specify the pulling strength from each link
// strength from each node
// Saving a reference to node attribute
var nodeForce = d3.forceManyBody().strength(-30);
var simulation = d3
.forceSimulation(graph.nodes)
.force("links", linkForce)
.force("charge", nodeForce)
.force("center", d3.forceCenter(width / 2, height / 2))
.on("tick", ticked);
var div = d3
.select("body")
.append("div")
.attr("class", "tooltip")
.style("opacity", 0);
svg
.append("defs")
.append("marker")
.attrs({
id: "arrowhead",
viewBox: "-0 -5 10 10",
refX: 100,
refY: 0,
orient: "auto-start-reverse",
markerWidth: 3,
markerHeight: 3
})
.append("svg:path")
.attr("d", "M 0,-5 L 10 ,0 L 0,5")
.attr("fill", "black");
{
{
/* .call(
d3.zoom().on("zoom", function() {
console.log(d3.event.transform);
svg.attr("transform", d3.event.transform);
})
); */
}
}
var link = svg
.append("g")
.selectAll("line_class.line")
.data(graph.links)
.enter()
.append("line")
.attr("stroke-width", function(d) {
return Math.max(
((d.thickness - min_thickness) / (max_thickness - min_thickness + 0.01)) *
5,
3
);
})
.style("stroke", "pink")
.text("text", function(d) {
return d.linkname;
})
.on("mouseover", function(d) {
div
.transition()
.duration(200)
.style("opacity", 0.9);
div
.html("Name: " + d.linkname)
.style("left", d3.event.pageX + "px")
.style("top", d3.event.pageY - 28 + "px")
.style("width", "auto")
.style("height", "auto");
})
.on("mouseout", function(d) {
div
.transition()
.duration(500)
.style("opacity", 0);
})
.attr("marker-start", "url(#arrowhead)");
// .attr("marker-start", "url(#arrowhead)")
// .attr("marker-mid", "url(#arrowhead)")
// .attr("marker-end", "url(#arrowhead)");
// var line = link
var edgepaths = svg
.selectAll(".edgepath")
.data(graph.links)
.enter()
.append("path")
.attr("d", function(d) {
return (
"M " +
d.source.x +
" " +
d.source.y +
" L " +
d.target.x +
" " +
d.target.y
);
})
.attr("class", "edgepath")
.attr("fill-opacity", 0)
.attr("stroke-opacity", 0)
.attr("fill", "blue")
.attr("stroke", "red")
.attr("id", function(d, i) {
return "edgepath" + i;
})
.style("pointer-events", "none");
var radius = 4;
var node_data_array = [];
var node_name = "";
var node = svg
.append("g")
.selectAll("circle_class.circle")
.data(graph.nodes)
.enter()
.append("rect")
.attr("width", 40)
.attr("height", 20)
.attr("fill", "url(#gradient)")
.style("transform", "translate(-20px,-10px)")
.attr("stroke", "purple")
.attr("id", function(d) {
node_name = d.name;
return d.name;
})
.on("dblclick", connectedNodes);
var textElements_nodes = svg
.append("g")
.selectAll("text")
.data(graph.nodes)
.enter()
.append("text")
.text(function(d) {
return d.name;
})
.attr("font-size", 15)
.attr("dx", 15)
.attr("dy", -5);
node.call(dragDrop);
link.call(updateState1);
var path = document.querySelector("path"),
totalLength = path.getTotalLength(),
group = totalLength / 20,
start;
var arrowheads = d3
.select("svg")
.selectAll("use")
.data(
d3.range(20).map(function(d) {
return d * group + 50;
})
)
.enter()
.append("use")
.attr("xlink:href", "#arrowhead");
path.style.strokeDasharray = "50," + (group - 50);
requestAnimationFrame(update);
function update(t) {
if (!start) {
start = t;
}
var offset = (-group * ((t - start) % 900)) / 900;
path.style.strokeDashoffset = offset;
arrowheads.attr("transform", function(d) {
var l = d - offset;
if (l < 0) {
l = totalLength + l;
} else if (l > totalLength) {
l -= totalLength;
}
var p = pointAtLength(l);
return "translate(" + p + ") rotate( " + angleAtLength(l) + ")";
});
//requestAnimationFrame(update);
}
function pointAtLength(l) {
var xy = path.getPointAtLength(l);
return [xy.x, xy.y];
}
// Approximate tangent
function angleAtLength(l) {
var a = pointAtLength(Math.max(l - 0.01, 0)), // this could be slightly negative
b = pointAtLength(l + 0.01); // browsers cap at total length
return (Math.atan2(b[1] - a[1], b[0] - a[0]) * 180) / Math.PI;
}
// link.attr("marker-end", "url(#end)");
function updateState1() {
link.each(function(d) {
var colors = ["red", "green", "blue"];
var num = 0;
if (d.source.name.startsWith("Dummy")) {
num = 1;
console.log("Inside 1");
console.log(
"Source is ",
d.source.name,
" and target is ",
d.target.name
);
} else if (d.target.name.startsWith("Dummy")) {
num = 2;
console.log("Inside 2");
console.log(
"Source is ",
d.source.name,
" and target is ",
d.target.name
);
} else {
num = 0;
for (i = 0; i < graph.input_nodes.length; i++) {
if (graph.input_nodes[i].name == d.source.name) {
num = 1;
}
}
}
d3.select(this).style("stroke", function(d) {
return colors[num];
});
{
{
/* d3.select(this).attr("marker-end", "url(#arrowhead)"); */
}
}
{
{
/* d3.select(this).attr("marker-mid", "url(#arrowhead)"); */
}
}
// .attr("marker-end", "url(#arrowhead)");
});
}
function pointOnRect(x, y, minX, minY, maxX, maxY, validate) {
//assert minX <= maxX;
//assert minY <= maxY;
if (validate && minX < x && x < maxX && minY < y && y < maxY)
throw "Point " +
[x, y] +
"cannot be inside " +
"the rectangle: " +
[minX, minY] +
" - " +
[maxX, maxY] +
".";
var midX = (minX + maxX) / 2;
var midY = (minY + maxY) / 2;
// if (midX - x == 0) -> m == ±Inf -> minYx/maxYx == x (because value / ±Inf = ±0)
var m = (midY - y) / (midX - x);
if (x <= midX) {
// check "left" side
var minXy = m * (minX - x) + y;
if (minY <= minXy && minXy <= maxY) return { x: minX, y: minXy };
}
if (x >= midX) {
// check "right" side
var maxXy = m * (maxX - x) + y;
if (minY <= maxXy && maxXy <= maxY) return { x: maxX, y: maxXy };
}
if (y <= midY) {
// check "top" side
var minYx = (minY - y) / m + x;
if (minX <= minYx && minYx <= maxX) return { x: minYx, y: minY };
}
if (y >= midY) {
// check "bottom" side
var maxYx = (maxY - y) / m + x;
if (minX <= maxYx && maxYx <= maxX) return { x: maxYx, y: maxY };
}
// edge case when finding midpoint intersection: m = 0/0 = NaN
if (x === midX && y === midY) return { x: x, y: y };
// Should never happen :) If it does, please tell me!
throw "Cannot find intersection for " +
[x, y] +
" inside rectangle " +
[minX, minY] +
" - " +
[maxX, maxY] +
".";
}
var label_toggle = 0;
function show_hide_node_labels() {
if (label_toggle) {
textElements_nodes.style("visibility", "visible");
} else {
textElements_nodes.style("visibility", "hidden");
}
label_toggle = !label_toggle;
}
var toggle = 0;
var linkedByIndex = {};
for (i = 0; i < graph.nodes.length; i++) {
linkedByIndex[i + "," + i] = 1;
}
graph.links.forEach(function(d) {
linkedByIndex[d.source.index + "," + d.target.index] = 1;
});
//DAT GUI for controls
var gui = new dat.GUI({ width: 300 });
config = {
linkStrength: 1,
linkDistance: 180,
nodeStrength: -30,
Width: 40,
Height: 20,
restart: reset,
showHideNodeLabels: show_hide_node_labels
};
var linkDistanceChanger = gui
.add(config, "linkDistance", 0, 400)
.step(1)
.name("Link Distance");
linkDistanceChanger.onChange(function(value) {
linkForce.distance(value);
simulation.alpha(1).restart();
});
var linkStrengthChanger = gui
.add(config, "linkStrength", 0, 1)
.name("Link Strength");
linkStrengthChanger.onChange(function(value) {
linkForce.strength(value);
simulation.alpha(1).restart();
});
var nodeStrengthChanger = gui
.add(config, "nodeStrength", -500, -1)
.step(1)
.name("Node Strength");
nodeStrengthChanger.onChange(function(value) {
nodeForce.strength(value);
simulation.alpha(1).restart();
});
var widthChanger = gui
.add(config, "Width", 4, 100)
.step(1)
.name("Node Width");
widthChanger.onChange(function(value) {
node.attr("width", value);
og_width = value;
// d3.select("#arrowhead").attrs({
// markerWidth: value * 0.4,
// markerHeight: value * 0.4
// });
simulation.alpha(1).restart();
});
var heightChanger = gui
.add(config, "Height", 2, 80)
.step(1)
.name("Node Height");
heightChanger.onChange(function(value) {
node.attr("height", value);
og_height = value;
// d3.select("#arrowhead").attrs({
// markerWidth: value * 0.4,
// markerHeight: value * 0.4
// });
simulation.alpha(1).restart();
});
gui.add(config, "showHideNodeLabels").name("Show/Hide Node Labels");
gui.add(config, "restart").name("Restart");
for (i = 0; i < gui.__ul.childNodes.length; i++) {
gui.__ul.childNodes[i].classList += " longtext";
}
function reset() {
window.location.reload();
}
//This function looks up whether a pair are neighbours
function neighboring(a, b) {
return linkedByIndex[a.index + "," + b.index];
}
function connectedNodes() {
if (toggle == 0) {
//Reduce the opacity of all but the neighbouring nodes
d = d3.select(this).node().__data__;
node.style("opacity", function(o) {
return neighboring(d, o) | neighboring(o, d) ? 1 : 0.1;
});
link.style("opacity", function(o) {
return (d.index == o.source.index) | (d.index == o.target.index)
? 1
: 0.1;
});
textElements_nodes.style("opacity", function(o) {
return neighboring(d, o) | neighboring(o, d) ? 1 : 0.1;
});
toggle = 1;
} else {
//Put them back to opacity=1
node.style("opacity", 1);
link.style("opacity", 1);
textElements_nodes.style("opacity", 1);
toggle = 0;
}
}
function ticked() {
link
.attr("x1", function(d) {
return d.source.x;
})
.attr("y1", function(d) {
return d.source.y;
})
.attr("x2", function(d) {
return d.target.x;
})
.attr("y2", function(d) {
return d.target.y;
})
.attr("d", function(d) {
var inter = pointOnRect(
d.source.x,
d.source.y,
d.target.x - 20,
d.target.y - 20,
d.target.x + 40 - 20,
d.target.y + 20 - 20
);
return (
"M" + d.source.x + "," + d.source.y + "L" + inter.x + "," + inter.y
);
});
node
.attr("x", function(d) {
return d.x;
})
.attr("y", function(d) {
return d.y;
});
textElements_nodes.attr("x", node => node.x).attr("y", node => node.y);
edgepaths.attr("d", function(d) {
var path =
"M " +
d.source.x +
" " +
d.source.y +
" L " +
d.target.x +
" " +
d.target.y;
return path;
});
var ticking = false;
}
var pulse = false;
function pulsed(rect) {
(function repeat() {
if (pulse) {
rect
.transition()
.duration(200)
.attr("stroke-width", 0)
.attr("stroke-opacity", 0)
.attr("width", config.Width)
.attr("height", config.Height)
.transition()
.duration(200)
.attr("stroke-width", 0)
.attr("stroke-opacity", 0.5)
.attr("width", config.Width * 3)
.attr("height", config.Height * 3)
.transition()
.duration(400)
.attr("stroke-width", 65)
.attr("stroke-opacity", 0)
.attr("width", config.Width)
.attr("height", config.Height)
.ease(d3.easeSin)
.on("end", repeat);
} else {
ticking = false;
rect
.attr("width", config.Width)
.attr("height", config.Height)
.attr("fill", "url(#gradient)")
.attr("stroke", "purple");
}
})();
}
function highlightNode() {
var userInput = document.getElementById("targetNode");
var temp = userInput.value;
var userInputRefined = temp.replace(/[/]/g, "\\/");
// make userInput work with "/" as they are considered special characters
// the char "/" is escaped with escape characters.
theNode = d3.select("#" + userInputRefined);
const isEmpty = theNode.empty();
if (isEmpty) {
document.getElementById("output").innerHTML = "Given node doesn't exist";
} else {
document.getElementById("output").innerHTML = "";
}
pulse = true;
if (pulse) {
pulsed(theNode);
}
scalingFactor = 0.5;
// Create a zoom transform from d3.zoomIdentity
var transform = d3.zoomIdentity
.translate(
screen.width / 2 - scalingFactor * theNode.attr("x"),
screen.height / 4 - scalingFactor * theNode.attr("y")
)
.scale(scalingFactor);
// Apply the zoom and trigger a zoom event:
svg1.call(zoom.transform, transform);
}
function minimizeNode() {
var userInput = document.getElementById("targetNode");
var temp = userInput.value;
var userInputRefined = temp.replace(/[/]/g, "\\/");
// make userInput work with "/" as they are considered special characters
// the char "/" is escaped with escape characters.
theNode = d3.select("#" + userInputRefined);
const isEmpty = theNode.empty();
if (isEmpty) {
document.getElementById("output").innerHTML = "Given node doesn't exist";
} else {
document.getElementById("output").innerHTML = "";
}
pulse = false;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.0.0/d3.min.js"></script>
<!DOCTYPE html>
<html>
<head>
<link rel="icon" href="data:;base64,iVBORw0KGgo=" />
<title>Force Layout Example 9</title>
<style>
div.tooltip {
position: absolute;
text-align: center;
width: 60px;
height: 28px;
padding: 2px;
font: 12px sans-serif;
background: lightsteelblue;
border: 0px;
border-radius: 8px;
pointer-events: none;
}
.longtext {
line-height: 13px;
height: 40px;
font-size: 120%;
}
rect.zoom-panel {
cursor: move;
fill: #fff;
pointer-events: all;
}
.bar {
fill: rgb(70, 180, 70);
}
.graph-svg-component {
background-color: green;
}
path {
fill: none;
stroke: #d3008c;
stroke-width: 2px;
}
#arrowhead {
fill: #d3008c;
stroke: none;
}
</style>
</head>
<body>
<svg></svg>
<div id="content"></div>
<script src="http://d3js.org/d3.v5.min.js"></script>
<script src="https://d3js.org/d3-dispatch.v1.min.js"></script>
<script src="https://d3js.org/d3-selection.v1.min.js"></script>
<script src="https://d3js.org/d3-drag.v1.min.js"></script>
<script src="https://d3js.org/d3-selection-multi.v1.min.js"></script>
<script src="https://d3js.org/d3-transition.v1.min.js"></script>
<script
type="text/javascript"
src="https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.7.6/dat.gui.min.js"
></script>
<link
href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/themes/base/jquery-ui.css"
rel="stylesheet"
type="text/css"
/>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>
</body>
</html>
I think this is what you want, this code snippet based on this bl.ocks.org snippet and it meant to be animated by I removed the requestAnimationFrame call from the update function.
regarding your original issue with nodes hiding the arrow, you can use refX and refY attribute on <marker> to change its offset.
var path = document.querySelector("path"),
totalLength = path.getTotalLength(),
group = totalLength / 20,
start;
var arrowheads = d3.select("svg").selectAll("use")
.data(d3.range(20).map(function(d){ return d * group + 50; }))
.enter()
.append("use")
.attr("xlink:href", "#arrowhead");
path.style.strokeDasharray = "50," + (group - 50);
requestAnimationFrame(update);
function update(t) {
if (!start) {
start = t;
}
var offset = -group * ((t - start) % 900) / 900;
path.style.strokeDashoffset = offset;
arrowheads.attr("transform",function(d){
var l = d - offset;
if (l < 0) {
l = totalLength + l;
} else if (l > totalLength) {
l -= totalLength;
}
var p = pointAtLength(l);
return "translate(" + p + ") rotate( " + angleAtLength(l) + ")";
});
//requestAnimationFrame(update);
}
function pointAtLength(l) {
var xy = path.getPointAtLength(l);
return [xy.x, xy.y];
}
// Approximate tangent
function angleAtLength(l) {
var a = pointAtLength(Math.max(l - 0.01,0)), // this could be slightly negative
b = pointAtLength(l + 0.01); // browsers cap at total length
return Math.atan2(b[1] - a[1], b[0] - a[0]) * 180 / Math.PI;
}
path {
fill: none;
stroke: #d3008c;
stroke-width: 2px;
}
#arrowhead {
fill: #d3008c;
stroke: none;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
</head>
<body>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="960" height="500">
<path d="M636.5,315c-0.4-18.7,1.9-27.9-5.3-35.9
c-22.7-25-107.3-2.8-118.3,35.9c-7,24.4,20.6,37.2,16,71c-4,29.6-30.8,60.7-56.5,61.1c-30.8,0.4-32.9-43.8-81.7-70.2
c-50.9-27.6-110.1-12.9-125.2-9.2c-66.1,16.4-82.2,56.9-109.2,47.3c-38-13.6-55.9-112.1-19.8-143.5c39-34,121.2,27.7,148.1-3.8
c18-21.1,3.1-74.3-25.2-105.3c-31.1-34.1-70.1-32.4-105.3-76.3c-8.2-10.2-16.9-23.8-15.3-39.7c1.2-11.4,7.5-23.3,15.3-29
c33.8-25,101.6,62.6,193.1,59.5c40.1-1.3,38.7-18.5,99.2-38.9c126.2-42.6,242.4-4.9,297.7,13c54.7,17.7,105.4,35,129.8,82.4
c13,25.3,22.9,67.7,4.6,87c-11.6,12.3-25.1,5.1-46.6,20.6c-2.8,2-28.9,21.4-32.1,49.6c-3.1,27.4,18.7,35,29,70.2
c8.8,30.1,8.5,77.8-18.3,99.2c-32.3,25.8-87,0.6-100-5.3c-69.6-32-67.2-88.4-73.3-109.2z"/>
<defs>
<path id="arrowhead" d="M7,0 L-7,-5 L-7,5 Z" />
</defs>
</svg>
<script src="https://d3js.org/d3.v4.min.js"></script>

How to stop text transitioning off the screen

I have a visual of a two ringed pie chart.
What I'm building is the following:
- if a sector's radians are too small for the specified text then the text is hidden. This is done and seems to work ok
- if text is hidden then a label should appear outside the pie. This is also done when the visual is initially rendered.
When the radio button is pressed the labels outside the pie should transition accordingly. I've attempted the transition and slowed it to 3500 and these labels are just slowly transitioning off the screen.
How do I fix this transition?
The snippet I've added to try to create the transition starts at line 241:
var arcs2 = svg.data([json]).selectAll(".arcG");
arcs2.data(partition.nodes)
.transition()
.duration(3500)
.attr("transform", function(d) {
var c = arc.centroid(d),
x = c[0],
y = c[1],
// pythagorean theorem for hypotenuse
h = Math.sqrt(x * x + y * y);
return "translate(" + (x / h * labelr) + ',' +
(y / h * labelr) + ")";
})
.attr("text-anchor", "middle");
svg.selectAll(".theTxtsOuter")
.text(function(d, i) {
if (d.name === 'root') {
return;
} else if ((d.depth === 1) && (d.dx < (d.name.length * 0.15))) {
return d.name;
} else if ((d.depth === 2) && (d.dx < (d.name.length * 0.1))) {
return d.name;
} else {
return;
}
});
This is a plunk of the working(!) visual:
https://plnkr.co/edit/jYVPCL?p=preview
Here is the complete javascript used by the pie:
function pieChart(dataFile) {
var plot;
var vis;
var width = 400,
height = 400,
radius = Math.min(width, height) / 2.1,
color = d3.scale.ordinal()
.range(["#338ABA", "#016da9", "#4c96d5"])
.domain([0, 2]);
var labelr = radius + 5 // radius for label anchor
var div = d3.select("body")
.append("div")
.attr("class", "toolTip");
var arc = d3.svg.arc()
.startAngle(function(d) {
return d.x;
})
.endAngle(function(d) {
return d.x + d.dx;
})
.outerRadius(function(d) {
return (d.y + d.dy) / (radius);
})
.innerRadius(function(d) {
return d.y / (radius);
});
//check if the svg already exists
plot = d3.select("#svgPIEChart");
if (plot.empty()) {
vis = d3.select("#pieChart")
.append("svg")
.attr({
id: "svgPIEChart"
});
} else {
vis = d3.select("#svgPIEChart");
vis.selectAll("*").remove();
}
//group of the svg element
var svg = vis
.append("g")
.attr({
'transform': "translate(" + width / 2 + "," + height * .52 + ")"
});
//svg element
vis.attr({
//set the width and height of our visualization (these will be attributes of the <svg> tag
width: width,
height: height
});
d3.text(dataFile, function(text) {
var csv = d3.csv.parseRows(text);
var json = buildHierarchy(csv);
// it seems d3.layout.partition() can be either squares or arcs
var partition = d3.layout.partition()
.sort(null)
.size([2 * Math.PI, radius * radius])
.value(function(d) {
return d.SalesRev;
});
var path = svg.data([json]).selectAll(".theArc")
.data(partition.nodes)
.enter()
.append("path")
.attr("class", "theArc")
.attr("id", function(d, i) {
return "theArc_" + i;
}) //Give each slice a unique ID
.attr("display", function(d) {
return d.depth ? null : "none";
})
.attr("d", arc)
.style("stroke", "#fff")
.style("fill", function(d) {
return color((d.children ? d : d.parent).name);
})
.attr("fill-rule", "evenodd")
.style("opacity", 0.01)
.style("stroke-opacity", 0.01)
.each(stash);
path.transition()
.duration(PIEOBJ.transTime)
.style("opacity", 1)
.style("stroke-opacity", 1)
path
.on("mouseout", mouseout)
.on("mousemove", function(d) {
div.style("left", d3.event.pageX + 10 + "px");
div.style("top", d3.event.pageY - 25 + "px");
div.style("display", "inline-block");
div.html(d.name + "<br>" + PIEOBJ.formatShrtInt(d.SalesRev));
})
var txts = svg.data([json]).selectAll(".theTxts")
.data(partition.nodes)
.enter()
.append("text");
txts
.attr("class", "theTxts")
.attr("dx", 10) //Move the text from the start angle of the arc
.attr("dy", 15) //Move the text down
.style("opacity", 0)
txts
.transition()
.duration(PIEOBJ.transTime)
.style("opacity", 1);
var txtPths = txts.append("textPath")
// .attr("xlink:href", function(d, i) {
.attr("href", function(d, i) {
return "#theArc_" + i;
})
.text(function(d) {
if (d.name === 'root') {
return;
} else if ((d.depth === 1) && (d.dx < (d.name.length * 0.15))) {
return;
} else if ((d.depth === 2) && (d.dx < (d.name.length * 0.1))) {
return;
} else {
return d.name;
}
});
/* ------- TEXT LABELS OUTSIDE THE PIE-------*/
//var arcs = svg.selectAll(".theArc");
var arcs = svg.data([json]).selectAll(".arcG")
.data(partition.nodes)
.enter()
.append("g")
.attr("class", "arcG");
arcs.append("text")
.attr("transform", function(d) {
var c = arc.centroid(d),
x = c[0],
y = c[1],
// pythagorean theorem for hypotenuse
h = Math.sqrt(x * x + y * y);
console.log(c, h);
return "translate(" + (x / h * labelr) + ',' +
(y / h * labelr) + ")";
})
.attr("text-anchor", "middle")
.text(function(d, i) {
if (d.name === 'root') {
return;
} else if ((d.depth === 1) && (d.dx < (d.name.length * 0.15))) {
return d.name;
} else if ((d.depth === 2) && (d.dx < (d.name.length * 0.1))) {
return d.name;
} else {
return;
}
})
.attr("class", "theTxtsOuter");
/* ----------------------------------------*/
d3.selectAll("input").on("change", function change() {
function createValueFunc(val) {
// currentMeasure = val;
return function(d) {
return d[val];
};
}
value = createValueFunc(this.value);
PIEOBJ.currentMeasure = this.value;
var path2 = svg.data([json]).selectAll(".theArc");
path2
.data(partition.value(value).nodes)
.transition()
.duration(1500)
.attrTween("d", arcTween)
.each("start", function() {
d3.select(this)
.on("mouseout", null) //CLEARING the listeners
.on("mousemove", null);
})
.each("end", function() {
d3.select(this)
.on("mouseout", mouseout) //attaching the listeners
.on("mousemove", function(d) {
div.style("left", d3.event.pageX + 10 + "px");
div.style("top", d3.event.pageY - 25 + "px");
div.style("display", "inline-block");
div.html(d.name + "<br>" + PIEOBJ.formatShrtInt(value(d)));
});
});
svg.selectAll("textPath")
.text(function(d) {
if (d.name === 'root') {
return;
} else if ((d.depth === 1) && (d.dx < (d.name.length * 0.15))) {
return;
} else if ((d.depth === 2) && (d.dx < (d.name.length * 0.1))) {
return;
} else {
return d.name;
}
});
var arcs2 = svg.data([json]).selectAll(".arcG");
arcs2.data(partition.nodes)
.transition()
.duration(3500)
.attr("transform", function(d) {
var c = arc.centroid(d),
x = c[0],
y = c[1],
// pythagorean theorem for hypotenuse
h = Math.sqrt(x * x + y * y);
return "translate(" + (x / h * labelr) + ',' +
(y / h * labelr) + ")";
})
.attr("text-anchor", "middle");
svg.selectAll(".theTxtsOuter")
.text(function(d, i) {
if (d.name === 'root') {
return;
} else if ((d.depth === 1) && (d.dx < (d.name.length * 0.15))) {
return d.name;
} else if ((d.depth === 2) && (d.dx < (d.name.length * 0.1))) {
return d.name;
} else {
return;
}
});
// the following deletes what was originally created and then recreates the text
// svg.selectAll("#titleX").remove();
});
function mouseout() {
div.style("display", "none"); //<< gets rid of the tooltip <<
}
// Stash the old values for transition.
function stash(d) {
d.x0 = d.x;
d.dx0 = d.dx;
}
// Interpolate the arcs in data space.
function arcTween(a) {
var i = d3.interpolate({
x: a.x0,
dx: a.dx0
}, a);
return function(t) {
var b = i(t);
a.x0 = b.x;
a.dx0 = b.dx;
return arc(b);
};
}
});
}
// // Take a 2-column CSV and transform it into a hierarchical structure suitable
// // for a partition layout.
function buildHierarchy(csv) {
var root = {
"name": "root",
"children": []
};
for (var i = 0; i < csv.length; i++) {
var sequence = csv[i][0];
// var APD = +csv[i][1];
var SalesRev = +csv[i][1];
var Amount = +csv[i][2];
if (isNaN(SalesRev)) { // e.g. if this is a header row
continue;
}
var parts = sequence.split("-");
var currentNode = root;
for (var j = 0; j < parts.length; j++) {
var children = currentNode.children;
var nodeName = parts[j];
var childNode;
if (j + 1 < parts.length) {
// Not yet at the end of the sequence; move down the tree.
var foundChild = false;
for (var k = 0; k < children.length; k++) {
if (children[k].name == nodeName) {
childNode = children[k];
foundChild = true;
break;
}
}
// If we don't already have a child node for this branch, create it.
if (!foundChild) {
childNode = {
"name": nodeName,
"children": []
};
children.push(childNode);
}
currentNode = childNode;
} else {
// Reached the end of the sequence; create a leaf node.
childNode = {
"name": nodeName,
// "APD": APD,
"SalesRev": SalesRev,
"Amount": Amount
};
children.push(childNode);
}
}
}
root.children.forEach(function(v) {
v.SalesRev = 0;
v.Amount = 0;
v.children.forEach(function(a) {
v.SalesRev += a.SalesRev;
v.Amount += a.Amount;
});
});
return root;
}
When you initially position them you are transforming the text elements. When you transition them you are positioning the outer g elements. These causes conflicting transforms. Use:
arcs2.data(partition.nodes)
.select('text') //<-- apply on child text
.transition()
.duration(3500)
.attr("transform", function(d) {
...
});
Updated plunker.

Animate objects in force layout in D3.js

I need to create a data visualisation which would look like a bunch of floating bubbles with text inside of the bubble.
I have a partially working example which uses mock data prepared here:
JSfiddle
// helpers
var random = function(min, max) {
if (max == null) {
max = min;
min = 0;
}
return min + Math.floor(Math.random() * (max - min + 1));
};
// mock data
var colors = [
{
fill: 'rgba(242,216,28,0.3)',
stroke: 'rgba(242,216,28,1)'
},
{
fill: 'rgba(207,203,196,0.3)',
stroke: 'rgba(207,203,196,1)'
},
{
fill: 'rgba(0,0,0,0.2)',
stroke: 'rgba(100,100,100,1)'
}
];
var data = [];
for(var j = 0; j <= 2; j++) {
for(var i = 0; i <= 4; i++) {
var text = 'text' + i;
var category = 'category' + j;
var r = random(50, 100);
data.push({
text: text,
category: category,
r: r,
r_change_1: r + random(-20, 20),
r_change_2: r + random(-20, 20),
fill: colors[j].fill,
stroke: colors[j].stroke
});
}
}
// mock debug
//console.table(data);
// collision detection
// derived from http://bl.ocks.org/mbostock/1748247
function collide(alpha) {
var quadtree = d3.geom.quadtree(data);
return function(d) {
var r = d.r + 10,
nx1 = d.x - r,
nx2 = d.x + r,
ny1 = d.y - r,
ny2 = d.y + r;
quadtree.visit(function(quad, x1, y1, x2, y2) {
if (quad.point && (quad.point !== d)) {
var x = d.x - quad.point.x,
y = d.y - quad.point.y,
l = Math.sqrt(x * x + y * y),
r = d.r * 2;
if (l < r) {
l = (l - r) / l * alpha;
d.x -= x *= l;
d.y -= y *= l;
quad.point.x += x;
quad.point.y += y;
}
}
return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
});
};
}
// initialize
var container = d3.select('.bubble-cloud');
var $container = $('.bubble-cloud');
var containerWidth = $container.width();
var containerHeight = $container.height();
var svgContainer = container
.append('svg')
.attr('width', containerWidth)
.attr('height', containerHeight);
// prepare layout
var force = d3.layout
.force()
.size([containerWidth, containerHeight])
.gravity(0)
.charge(0)
;
// load data
force.nodes(data)
.start()
;
// create item groups
var node = svgContainer.selectAll('.node')
.data(data)
.enter()
.append('g')
.attr('class', 'node')
.call(force.drag);
// create circles
node.append('circle')
.classed('circle', true)
.attr('r', function (d) {
return d.r;
})
.style('fill', function (d) {
return d.fill;
})
.style('stroke', function (d) {
return d.stroke;
});
// create labels
node.append('text')
.text(function(d) {
return d.text
})
.classed('text', true)
.style({
'fill': '#ffffff',
'text-anchor': 'middle',
'font-size': '12px',
'font-weight': 'bold',
'font-family': 'Tahoma, Arial, sans-serif'
})
;
node.append('text')
.text(function(d) {
return d.category
})
.classed('category', true)
.style({
'fill': '#ffffff',
'font-family': 'Tahoma, Arial, sans-serif',
'text-anchor': 'middle',
'font-size': '9px'
})
;
node.append('line')
.classed('line', true)
.attr('x1', 0)
.attr('y1', 0)
.attr('x2', 50)
.attr('y2', 0)
.attr('stroke-width', 1)
.attr('stroke', function (d) {
return d.stroke;
})
;
// put circle into movement
force.on('tick', function(){
d3.selectAll('circle')
.each(collide(.5))
.attr('cx', function (d) {
// boundaries
if(d.x <= d.r) {
d.x = d.r + 1;
}
if(d.x >= containerWidth - d.r) {
d.x = containerWidth - d.r - 1;
}
return d.x;
})
.attr('cy', function (d) {
// boundaries
if(d.y <= d.r) {
d.y = d.r + 1;
}
if(d.y >= containerHeight - d.r) {
d.y = containerHeight - d.r - 1;
}
return d.y;
});
d3.selectAll('line')
.attr('x1', function (d) {
return d.x - d.r + 10;
})
.attr('y1', function (d) {
return d.y;
})
.attr('x2', function (d) {
return d.x + d.r - 10;
})
.attr('y2', function (d) {
return d.y;
});
d3.selectAll('.text')
.attr('x', function (d) {
return d.x;
})
.attr('y', function (d) {
return d.y - 10;
});
d3.selectAll('.category')
.attr('x', function (d) {
return d.x;
})
.attr('y', function (d) {
return d.y + 20;
});
});
// animate
var interval = setInterval(function(){
// moving of the circles
// ...
}, 5 * 1000);
However I am now facing problem with animation. I cannot figure out how can I animate nodes in force diagram. I tried to adjust values of the data object and then invoke .tick() method inside setInterval method, however it didn't help. I am utilizing D3 force layout.
My questions are:
How to make the bubbles "float" around the screen, i.e. how to
animate them?
How to animate changes of circle radius?
Thank you for your ideas.
Actually, I think this one feels nicer...
Key points
Charge set to 0, friction set to 0.9
Schedule parallel transitions on the radius and line in the timer callback
Use the dynamic radius for calculating collisions
Use a transform on the nodes (g element) to decouple text and line positioning from node position, adjust the transform x and y, only in the tick callback
Remove the CSS transitions and add d3 transitions so that you can synchronise everything
changed this r = d.rt + 10 to this r = d.rt + rmax in the collision function to tighten up the control on overlaps
Closed loop speed regulator. Even though friction is set to 0.9 to dampen movement, the speed regulator will keep them moving
Use parallel transitions to coordinate geometry changes
Added a small amount of gravity
working example
// helpers
var random = function(min, max) {
if (max == null) {
max = min;
min = 0;
}
return min + Math.floor(Math.random() * (max - min + 1));
},
metrics = d3.select('.bubble-cloud').append("div")
.attr("id", "metrics")
.style({"white-space": "pre", "font-size": "8px"}),
elapsedTime = outputs.ElapsedTime("#metrics", {
border: 0, margin: 0, "box-sizing": "border-box",
padding: "0 0 0 6px", background: "black", "color": "orange"
})
.message(function(value) {
var this_lap = this.lap().lastLap, aveLap = this.aveLap(this_lap)
return 'alpha:' + d3.format(" >7,.3f")(value)
+ '\tframe rate:' + d3.format(" >4,.1f")(1 / aveLap) + " fps"
}),
hist = d3.ui.FpsMeter("#metrics", {display: "inline-block"}, {
height: 8, width: 100,
values: function(d){return 1/d},
domain: [0, 60]
}),
// mock data
colors = [
{
fill: 'rgba(242,216,28,0.3)',
stroke: 'rgba(242,216,28,1)'
},
{
fill: 'rgba(207,203,196,0.3)',
stroke: 'rgba(207,203,196,1)'
},
{
fill: 'rgba(0,0,0,0.2)',
stroke: 'rgba(100,100,100,1)'
}
];
// initialize
var container = d3.select('.bubble-cloud');
var $container = $('.bubble-cloud');
var containerWidth = 600;
var containerHeight = 180 - elapsedTime.selection.node().clientHeight;
var svgContainer = container
.append('svg')
.attr('width', containerWidth)
.attr('height', containerHeight);
var data = [],
rmin = 15,
rmax = 30;
d3.range(0, 3).forEach(function(j){
d3.range(0, 6).forEach(function(i){
var r = random(rmin, rmax);
data.push({
text: 'text' + i,
category: 'category' + j,
x: random(rmax, containerWidth - rmax),
y: random(rmax, containerHeight - rmax),
r: r,
fill: colors[j].fill,
stroke: colors[j].stroke,
get v() {
var d = this;
return {x: d.x - d.px || 0, y: d.y - d.py || 0}
},
set v(v) {
var d = this;
d.px = d.x - v.x;
d.py = d.y - v.y;
},
get s() {
var v = this.v;
return Math.sqrt(v.x * v.x + v.y * v.y)
},
set s(s1){
var s0 = this.s, v0 = this.v;
if(!v0 || s0 == 0) {
var theta = Math.random() * Math.PI * 2;
this.v = {x: Math.cos(theta) * s1, y: Math.sin(theta) * s1}
} else this.v = {x: v0.x * s1/s0, y: v0.y * s1/s0};
},
set sx(s) {
this.v = {x: s, y: this.v.y}
},
set sy(s) {
this.v = {y: s, x: this.v.x}
},
});
})
});
// collision detection
// derived from http://bl.ocks.org/mbostock/1748247
function collide(alpha) {
var quadtree = d3.geom.quadtree(data);
return function(d) {
var r = d.rt + rmax,
nx1 = d.x - r,
nx2 = d.x + r,
ny1 = d.y - r,
ny2 = d.y + r;
quadtree.visit(function(quad, x1, y1, x2, y2) {
if (quad.point && (quad.point !== d)) {
var x = d.x - quad.point.x,
y = d.y - quad.point.y,
l = Math.sqrt(x * x + y * y),
r = d.rt + quad.point.rt;
if (l < r) {
l = (l - r) / l * (1 + alpha);
d.x -= x *= l;
d.y -= y *= l;
quad.point.x += x;
quad.point.y += y;
}
}
return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
});
};
}
// prepare layout
var force = d3.layout
.force()
.size([containerWidth, containerHeight])
.gravity(0.001)
.charge(0)
.friction(.8)
.on("start", function() {
elapsedTime.start(100);
});
// load data
force.nodes(data)
.start();
// create item groups
var node = svgContainer.selectAll('.node')
.data(data)
.enter()
.append('g')
.attr('class', 'node')
.call(force.drag);
// create circles
var circles = node.append('circle')
.classed('circle', true)
.attr('r', function (d) {
return d.r;
})
.style('fill', function (d) {
return d.fill;
})
.style('stroke', function (d) {
return d.stroke;
})
.each(function(d){
// add dynamic r getter
var n= d3.select(this);
Object.defineProperty(d, "rt", {get: function(){
return +n.attr("r")
}})
});
// create labels
node.append('text')
.text(function(d) {
return d.text
})
.classed('text', true)
.style({
'fill': '#ffffff',
'text-anchor': 'middle',
'font-size': '6px',
'font-weight': 'bold',
'text-transform': 'uppercase',
'font-family': 'Tahoma, Arial, sans-serif'
})
.attr('x', function (d) {
return 0;
})
.attr('y', function (d) {
return - rmax/5;
});
node.append('text')
.text(function(d) {
return d.category
})
.classed('category', true)
.style({
'fill': '#ffffff',
'font-family': 'Tahoma, Arial, sans-serif',
'text-anchor': 'middle',
'font-size': '4px'
})
.attr('x', function (d) {
return 0;
})
.attr('y', function (d) {
return rmax/4;
});
var lines = node.append('line')
.classed('line', true)
.attr({
x1: function (d) {
return - d.r + rmax/10;
},
y1: function (d) {
return 0;
},
x2: function (d) {
return d.r - rmax/10;
},
y2: function (d) {
return 0;
}
})
.attr('stroke-width', 1)
.attr('stroke', function (d) {
return d.stroke;
})
.each(function(d){
// add dynamic x getter
var n= d3.select(this);
Object.defineProperty(d, "lxt", {get: function(){
return {x1: +n.attr("x1"), x2: +n.attr("x2")}
}})
});
// put circle into movement
force.on('tick', function t(e){
var s0 = 0.25, k = 0.3;
a = e.alpha ? e.alpha : force.alpha();
elapsedTime.mark(a);
if(elapsedTime.aveLap.history.length)
hist(elapsedTime.aveLap.history);
for ( var i = 0; i < 3; i++) {
circles
.each(collide(a))
.each(function(d) {
var moreThan, v0;
// boundaries
//reflect off the edges of the container
// check for boundary collisions and reverse velocity if necessary
if((moreThan = d.x > (containerWidth - d.rt)) || d.x < d.rt) {
d.escaped |= 2;
// if the object is outside the boundaries
// manage the sign of its x velocity component to ensure it is moving back into the bounds
if(~~d.v.x) d.sx = d.v.x * (moreThan && d.v.x > 0 || !moreThan && d.v.x < 0 ? -1 : 1);
// if vx is too small, then steer it back in
else d.sx = (~~Math.abs(d.v.y) || Math.min(s0, 1)*2) * (moreThan ? -1 : 1);
// clear the boundary without affecting the velocity
v0 = d.v;
d.x = moreThan ? containerWidth - d.rt : d.rt;
d.v = v0;
// add a bit of hysteresis to quench limit cycles
} else if (d.x < (containerWidth - 2*d.rt) && d.x > 2*d.rt) d.escaped &= ~2;
if((moreThan = d.y > (containerHeight - d.rt)) || d.y < d.rt) {
d.escaped |= 4;
if(~~d.v.y) d.sy = d.v.y * (moreThan && d.v.y > 0 || !moreThan && d.v.y < 0 ? -1 : 1);
else d.sy = (~~Math.abs(d.v.x) || Math.min(s0, 1)*2) * (moreThan ? -1 : 1);
v0 = d.v;
d.y = moreThan ? containerHeight - d.rt : d.rt;
d.v = v0;
} else if (d.y < (containerHeight - 2*d.rt) && d.y > 2*d.rt) d.escaped &= ~4;
});
}
// regulate the speed of the circles
data.forEach(function reg(d){
if(!d.escaped) d.s = (s0 - d.s * k) / (1 - k);
});
node.attr("transform", function position(d){return "translate(" + [d.x, d.y] + ")"});
force.alpha(0.05);
});
// animate
window.setInterval(function(){
var tinfl = 3000, tdefl = 1000, inflate = "elastic", deflate = "cubic-out";
for(var i = 0; i < data.length; i++) {
if(Math.random()>0.8) data[i].r = random(rmin,rmax);
}
var changes = circles.filter(function(d){return d.r != d.rt});
changes.filter(function(d){return d.r > d.rt})
.transition("r").duration(tinfl).ease(inflate)
.attr('r', function (d) {
return d.r;
});
changes.filter(function(d){return d.r < d.rt})
.transition("r").duration(tdefl).ease(deflate)
.attr('r', function (d) {
return d.r;
});
// this runs with an error of less than 1% of rmax
changes = lines.filter(function(d){return d.r != d.rt});
changes.filter(function(d){return d.r > d.rt})
.transition("l").duration(tinfl).ease(inflate)
.attr({
x1: function lx1(d) {
return -d.r + rmax / 10;
},
x2: function lx2(d) {
return d.r - rmax / 10;
}
});
changes.filter(function(d){return d.r < d.rt})
.transition("l").duration(tdefl).ease(deflate)
.attr({
x1: function lx1(d) {
return -d.r + rmax / 10;
},
x2: function lx2(d) {
return d.r - rmax / 10;
}
});
}, 2 * 500);
body {
background: black;
margin:0;
padding:0;
}
.bubble-cloud {
background: url("http://dummyimage.com/100x100/111/333?text=sample") 0 0;
width: 600px;
height: 190px;
overflow: hidden;
position: relative;
margin:0 auto;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<script src="https://gitcdn.xyz/repo/cool-Blue/d3-lib/master/elapsedTime/elapsed-time-2.0.js"></script>
<script src="https://gitcdn.xyz/repo/cool-Blue/d3-lib/master/plot/plot-transform.js"></script>
<script src="https://gitcdn.xyz/repo/cool-Blue/d3-lib/master/plot/fps-histogram.js"></script>
<link rel="stylesheet" type="text/css" href="https://gitcdn.xyz/repo/cool-Blue/d3-lib/master/plot/fps-histogram.css">
<div class="bubble-cloud"></div>
I like to use this formula for the spacing dynamic...
l = (l - r) / l * (1+ alpha);
and then use an alpha of about 0.05
No need for gravity or charge in my view, the only thing I change is to set friction to 1. This means that velocity is maintained, but if your clients are getting motion sick then knock it back to 0.99.
EDIT:
changed to a slightly softer and more correct collision model
l = (l - r) / l * (1/2 + alpha);
Also added a little gravity to make it "cloud-like" and friction (see above)
CSS transitions
I also tried to use CSS transitions but support seems patchy to say the least on SVG elements.
Transition works on circle radius but not on line in chrome (45.0) and Opera
In IE 11 and FF (40.0.3) none of the CSS transitions work for me
I would be interested in any feed-back on browser compatibility as i couldn't find much on the internet about this.
I experimented with velocity.js on the back of this and I think I prefer it for the transitions.

D3 force layout works in Chrome, but not in Firefox

I've created a force-layout using D3 (see image below). However, for some reason it does not work in Firefox, whereas it works perfectly fine in Chrome. There's no errors in the Firefox debugger, but it only shows me a single line in the right side of the browser (as if the force layout never updates). I'm debugging it using a local server and browsing at http://localhost:8888/.
I've been looking at different posts regarding compatibility on stackoverflow, but I can't seem to find anything related to my code. If someone could give me a header on what to debug first, that would be great!
Edit: I've included links to the data as well as the csv-files in plain text at the bottom of my post. Data and code: https://www.dropbox.com/s/ksh2qk1b5s9lfq5/Network%20View.zip?dl=0
Here's the output from the Firefox console:
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 d3.js:553:4
SyntaxError: An invalid or illegal string was specified d3.js:562:0
Chrome:
Firefox:
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);
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
The syntax error is with this line:
defs.select(current_gradient).remove();
But above that is the real problem. Replace:
current_gradient = current_gradient.substring(4, current_gradient.length - 1);
With:
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);
}
When you originally set the current_gradient in Chrome, it is set to "url(somevalue)" whereas in Firefox is it set to "url(fullpath/somevalue)". So you need to remove all the path info, not just the "url()" bit. Splitting the slash and taking the last value from the split is probably the easiest way to do that.

How to hide outer ring in zoomable sunburst

I wanted to hide the outer ring in Sunburst, and I wanted to show it when the user drills down the sunburst. So that I can clearly show the data in outer ring clearly.... Thanks in advance
Here is my code
var width = 700,
height = width,
radius = width / 2,
x = d3.scale.linear().range([0, 2 * Math.PI]),
y = d3.scale.pow().exponent(1.3).domain([0, 1]).range([0, radius]),
padding = 5,
duration = 1000;
color = d3.scale.category20c();
var div = d3.select("#vis");
div.select("svg").remove();
div.select("div").remove();
div.select("p").remove();
div.select("img").remove();
var vis = div.append("svg")
.attr("width", width + padding * 2)
.attr("height", height + (padding * 2) + 2000)
.append("g")
.attr("transform", "translate(" + [radius + padding, radius + padding] + ")");
/*
div.append("p")
.attr("id", "intro")
.text("Click to zoom!");
*/
var partition = d3.layout.partition()
.sort(null)
.value(function(d) { return 5.8 - d.depth; });
var arc = d3.svg.arc()
.startAngle(function(d) { return Math.max(0, Math.min(2 * Math.PI, x(d.x))); })
.endAngle(function(d) { return Math.max(0, Math.min(2 * Math.PI, x(d.x + d.dx))); })
.innerRadius(function(d) { return Math.max(0, d.y ? y(d.y) : d.y); })
.outerRadius(function(d) { return Math.max(0, y(d.y + d.dy)); });
var json = JSON.parse(jsonStr);
var nodes = partition.nodes({children: json});
var path = vis.selectAll("path").data(nodes);
path.enter().append("path")
.attr("id", function(d, i) { return "path-" + i; })
.attr("d", arc)
.attr("fill-rule", "evenodd")
.style("fill", colour)
.attr("title",function(d)
{
return d.depth ? d.desc: "";
})
.on("click", nodeClicked);
var text = vis.selectAll("text").data(nodes);
var textEnter = text.enter().append("text")
.style("fill-opacity", 1)
.style("fill", function(d)
{
return brightness(d3.rgb(colour(d))) < 125 ? "#eee" : "#000";
})
.attr("text-anchor", function(d)
{
return x(d.x + d.dx / 2) > Math.PI ? "end" : "start";
})
.attr("dy", ".2em")
.attr("transform", function(d)
{
var multiline = (d.name || "").split(" ").length > 1,
angle = x(d.x + d.dx / 2) * 180 / Math.PI - 90,
rotate = angle + (multiline ? -0.5 : 0);
return "rotate(" + rotate + ")translate(" + (y(d.y) + padding) + ")rotate(" + (angle > 90 ? -180 : 0) + ")";
})
.attr("title",function(d)
{
return d.depth ? d.desc: "";
})
.on("click", showModelDetailsForCriteria);
textEnter.append("tspan")
.attr("x", 0)
.text(function(d) { return d.depth ? d.name.split(" ")[0] : ""; });
textEnter.append("tspan")
.attr("x", 0)
.attr("dy", "1em")
.text(function(d) { return d.depth ? d.name.split(" ")[1] || "" : ""; });
function nodeClicked(d)
{
path.transition()
.duration(duration)
.attrTween("d", arcTween(d));
// Somewhat of a hack as we rely on arcTween updating the scales.
text.style("visibility", function(e)
{
return isParentOf(d, e) ? null : d3.select(this).style("visibility");
})
.transition()
.duration(duration)
.attrTween("text-anchor", function(d)
{
return function()
{
return x(d.x + d.dx / 2) > Math.PI ? "end" : "start";
};
})
.attrTween("transform", function(d)
{
var multiline = (d.name || "").split(" ").length > 1;
return function()
{
var angle = x(d.x + d.dx / 2) * 180 / Math.PI - 90,
rotate = angle + (multiline ? -0.5 : 0);
return "rotate(" + rotate + ")translate(" + (y(d.y) + padding) +
")rotate(" + (angle > 90 ? -180 : 0) + ")";
};
})
.style("fill-opacity", function(e)
{
return isParentOf(d, e) ? 1 : 1e-6;
})
.each("end", function(e)
{
d3.select(this).style("visibility", isParentOf(d, e) ? null : "hidden");
});
}
function isParentOf(p, c)
{
if (p === c) return true;
if (p.children)
{
return p.children.some(function(d)
{
return isParentOf(d, c);
});
}
return false;
}
function colour(d)
{
var nameStr = d.name;
if (d.name == 'Danger')
{
return "red";
}
else if (d.name == 'Stable')
{
return "green";
}
else if ((d.name !== undefined) && (nameStr.indexOf("Need Ana") >= 0))
{
return "orange";
}
else if ((d.name !== undefined) && (nameStr.indexOf("Not Mon") >= 0))
{
return "grey";
}
else
{
if (!d.parent)
{
return "#fff";
}
if (!useRAG)
{
if ((d.data.color !== null) && (d.data.color !== undefined) &&
((d.data.color == "red")||(d.data.color == "green")||(d.data.color == "amber")||(d.data.color == "grey")))
{
return d.data.color;
}
return color((d.children ? d : d.parent).name);
}
if (d.children)
{
var colours = d.children.map(colour);
var childCount = d.children.length;
var hueAddition = 10;
for (var index = 0; index < childCount; index++ )
{
hueAddition += (d3.hsl(colours[index])).h;
}
return d3.hsl((hueAddition) / childCount, (d3.hsl(colours[0])).s * 1.2,
(d3.hsl(colours[0])).l * 1.2);
}
return d.colour || "#fff";
}
}
// Interpolate the scales!
function arcTween(d)
{
var my = maxY(d),
xd = d3.interpolate(x.domain(), [d.x, d.x + d.dx]),
yd = d3.interpolate(y.domain(), [d.y, my]),
yr = d3.interpolate(y.range(), [d.y ? 20 : 0, radius]);
return function(d)
{
return function(t)
{
x.domain(xd(t)); y.domain(yd(t)).range(yr(t)); return arc(d);
};
};
}
function maxY(d)
{
return d.children ? Math.max.apply(Math, d.children.map(maxY)) : d.y + d.dy;
}
// http://www.w3.org/WAI/ER/WD-AERT/#color-contrast
function brightness(rgb)
{
return rgb.r * 0.299 + rgb.g * 0.587 + rgb.b * 0.114;
}
if (top != self) top.location.replace(location);
function showModelDetailsForCriteria(d)
{
var criterions = d.filter.split(",");
var confirmationMessage = "Do you want to view the model list for - ";
var startindex = 0;
if (criterions[startindex] !== null && criterions[startindex] !== undefined)
{
var statusInt = parseInt(criterions[startindex]);
var statusString = "Not Monitered";
if (statusInt === 0)
{
statusString = "Stable";
}
else if (statusInt == 1)
{
statusString = "Need Analysis";
}
else if (statusInt == 2)
{
statusString = "Danger";
}
confirmationMessage += "<BR>Health Status = " + statusString;
startindex++;
}
if (criterions[startindex] !== null && criterions[startindex] !== undefined)
{
confirmationMessage += "<BR>Model Owner = " + criterions[startindex];
startindex++;
}
if (criterions[startindex] !== null && criterions[startindex] !== undefined)
{
confirmationMessage += "<BR>Biz Application = " + criterions[startindex];
startindex++;
}
if (criterions[startindex] !== null && criterions[startindex] !== undefined)
{
confirmationMessage += "<BR>Class of Model = " + criterions[startindex];
startindex++;
}
if (criterions[startindex] !== null && criterions[startindex] !== undefined)
{
confirmationMessage += "<BR>Model Name = " + criterions[startindex];
startindex++;
}
if (criterions[startindex] !== null && criterions[startindex] !== undefined)
{
confirmationMessage += "<BR>Product Type = " + criterions[startindex];
startindex++;
}
app.confirm(confirmationMessage, false,
function()
{
nodeClicked(d);
},
function ()
{
//nodeClicked(d);
});
}
Look at this piece of code, you should be able to add the depth for each ring. You should know the max depth of the layout and that gives you your outer ring.
var path = vis.selectAll("path").data(nodes);
path.enter().append("path")
.attr("id", function(d, i) { return "path-" + i; })
.attr("class", function(d) { return "ring_" + d.depth; }) <--- add this line
.attr("d", arc)
.attr("fill-rule", "evenodd")
.style("fill", colour)
.attr("title",function(d)
{
return d.depth ? d.desc: "";
})
.on("click", nodeClicked);
Just in case you find some useful ideas here: http://protembla.com/wheel.html some unfinished viz I played with when I was just learning d3, so quality is not great!

Categories

Resources