Firefox的D3强制版面性能下降 [英] Slow performance in Firefox for D3 force layout
问题描述
我使用D3创建了一个强制布局(见下图)。但是,它在Firefox中运行速度非常缓慢,而在Chrome中运行得非常好。我正在使用本地服务器进行调试,并在 http:// localhost:8888 / 上进行浏览。这可能是由于在Firefox控制台中的以下消息,但相应的评论是不可能的。有人可以指出性能问题,并给我提示如何解决它?
突变的[[Prototype]]对象会导致你的代码运行非常缓慢;而是使用Object.create
创建具有正确初始[[Prototype]]值的对象数据和代码在zip中: https://www.dropbox.com/s / b Index.html networkview.js Links.csv Nodes.csv 编辑 根据我上面的评论,我设法通过进行以下更改来解决Firefox问题: 完成这些更改后,Firefox中的速度问题消失了,现在是三款主流浏览器的速度都一样。尽管在Chrome中看起来更好。一些试验是为了确定哪些变化是至关重要的,但是删除 我做的其他更改只是风格,使我更容易理解。 b 修正的代码: JS 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? Data and code in zip: https://www.dropbox.com/s/ksh2qk1b5s9lfq5/Network%20View.zip?dl=0 Visualization: Index.html networkview.js Links.csv Nodes.csv
EDIT As per my comments above, I managed to solve the Firefox problem by making the following changes: 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 The other changes I made were just stylistic to make it easier for me to understand. Amended code: JS
这篇关于Firefox的D3强制版面性能下降的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!
$ b $ $ b
<!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;
//笔划:black;
ody {
/ *不同浏览器的缩放比例* /
-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 / javascriptsrc =d3.js>< / script>
< script type =text / javascriptsrc =papaparse.js>< / script>
< script type =text / javascriptsrc =jquery.js>< / script>
< script type =text / javascriptsrc =networkview.js>< / script>
< / body>
var line_diff = 0.5; //如果你想在调用/文本行之间有空格,则从零增加
var mark_offset = 10; //每一端的标记线有多少百分比不用于传入/传出之间的关系?
var mark_size = 5; //行上标记的大小
var legendRectSize = 9; // 18
var legendSpacing = 4; // 4
var recordTypes = [];
var legend;
var text_links_data,call_links_data;
//可视化不同部分的颜色
recordTypes.push({
text:call,
color:#438DCA
});
recordTypes.push({
text:text,
color:#70C05A
});
recordTypes.push({
text:balance,
color:#245A76
});
//用于从数组中获取特定属性的函数
pluck = function(ary,prop){
return ary.map(function(x){
return x [prop]
});
$ sum
sum = function(ary){
return ary.reduce(function(a,b){
return a + b
},0);
$ b maxArray = function(ary){
return ary.reduce(function(a,b){
return Math.max(a,b)
},-Infinity);
$ b 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,
分隔符:,,
skipEmptyLines:true,
complete:function(results){
data_links = results.data;
dataLoaded();
}
});
var results = Papa.parse(nodes.csv,{
header:true,
download:true,
dynamicTyping:true,
分隔符:,,
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);
}
函数node_radius_data(d){
return Math.pow(40.0 * d.size,1/3);
函数dataLoaded(){
if(typeof data_nodes ===undefined|| typeof data_links ===undefined){
// console.log(Still loading)
} else {
CreateVisualizationFromData();
函数isConnectedToOtherThanMain(a){
var connected = false;如果(isConnected(a,data_nodes [i])&& a.index!= i){$ b $($ i
)(i = 1; i< data_nodes.length; i ++) b connected = true;
}
}
返回连接;
函数isConnected(a,b){
return isConnectedAsTarget(a,b)|| isConnectedAsSource(a,b)|| a.index == b.index;
函数isConnectedAsSource(a,b){
return linkedByIndex [a.index +,+ b.index];
函数isConnectedAsTarget(a,b){
return linkedByIndex [b.index +,+ a.index];
函数isEqual(a,b){
return a.index == b.index;
函数tick(){
if(call_links_data.length> 0){
callLink
.attr(x1 ,函数(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 ,函数(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)
});
.attr(transform,function(d){
returntranslate(+ dx +,+ dy +);
});
$ b if(force.alpha()<0.05)
drawLegend();
函数getRandomInt(){
返回Math.floor(Math.random()*(100000 - 0));
函数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); (current_gradient.match(http)){
var parts = current_gradient.split(/);
if
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; //缩放所以不会碰到两端
lineLengthCalculation = function(x,y,x0,y0){
return Math.sqrt((x - = x0)* x + (y - = y 0)* y);
};
lineLength = lineLengthCalculation(from.px,from.py,to.px,to.py);
if(lineLength> = 0.1){
mark_size_percent =(mark_size / lineLength)* 100;
$ b defs.append(linearGradient)
.attr(id,new_gradient_id)
.attr(gradientUnits,userSpaceOnUse)
.attr (x1from.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
$ {b $ b offset:Math.round(mid_offset - mark_size_percent / 2)+%,
color:standardColor,
opacity:1
},{
偏移量:Math.round(mid_offset - mark_size_percent / 2)+%,
c olor:#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; (stop-color,function(d){
return d.color;
})
.attr(stop-opacity ,函数(d){
返回d.opacity;
});
$ b $ self.style(stroke,url(#+ new_gradient_id +))
defs.select(current_gradient).remove();
}
}
var linkedByIndex;
var width = $(window).width();
var height = $(window).height();
$ b $ var svg = d3.select(body)。append(svg)
.attr(width,width)
.attr(height,高度);
var force;
var callLink;
var textLink;
var link;
var node;
var defs;
var total_interactions = 0;
var max_interactions = 0;
函数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);
$ b函数chargeForNode(d,i){
//主节点
if(i == 0){
return -25000;
//包含其他链接
else if(isConnectedToOtherThanMain(d)){
return -2000;
} else {
return -1200;
//节点的初始位置防止重叠
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 = center_y;}})
force = d3.layout.force()
.nodes(data_nodes)
.links(data_links)
.charge(function (0.6)// 0.6
。重力(0.4)// 0.6
$(
$ b $ .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);
$ b $ node = svg.selectAll(。node)
.data(data_nodes)
.enter()。append(g)
.attr( class,node);
defs = svg.append(defs);
$ b $ 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 L 6,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)
});
$ b $ if(call_links_data.length> 0){
callLink
.style(stroke-width,function stroke(d){
函数(d){
applyGradient(this,call,d)
});
}
force
.on(tick,tick);
$ b 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));
$ b $ 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 +')';
});
$ b $ legend.append('rect')
.attr('width',legendRectSize)
.attr('height',legendRectSize)
.style(' (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 //最宽行的宽度
函数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;
函数total_width(d){
return(d.inc_calls + d.out_calls + d.inc_texts + d.out_texts)/ max_interactions * line_width_factor + line_diff;
函数line_perpendicular_shift(d,direction){
theta = getAngle(d);
theta_perpendicular = theta +(Math.PI / 2)*方向;
lineWidthOfOppositeLine =方向== 1? text_width(d):call_width(d);
shift = lineWidthOfOppositeLine / 2;
$ b $ delta_x =(shift + line_diff)* Math.cos(theta_perpendicular)
delta_y =(shift + line_diff)* Math.sin(theta_perpendicular)
return [ delta_x,delta_y]
}
函数line_radius_shift_to_edge(d,which_node){// which_node = 0 if source,= 1 if if
theta = getAngle(d);
theta =(which_node == 0)? theta:theta + Math.PI; //如果目标节点为反向角度
radius =(which_node == 0)? node_radius(d.source):node_radius(d.target)// d.source和d.target直接指向节点(不是索引)
radius - = 2; //添加笔划宽度
delta_x =半径* Math.cos(theta)
delta_y =半径* Math.sin(theta)
return [delta_x,delta_y ]
}
函数getAngle(d){
rel_x = d.target.x - d.source.x;
rel_y = d.target.y - d.source.y;
返回theta = Math.atan2(rel_y,rel_x);
源,目标,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
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
问题的根本原因是由于未能清除 defs
部分中过时的 linearGradient
标签导致的文档膨胀的
HTML。这只是在Firefox中发生的,因为它返回的
响应 getPropertyValue
在它的 CSSStyleDeclaration
接口(这是在 selection.style()
)中由d3调用。
的返回值是
url(http:// localhost:88888 / index.html#line-gradientXXXXXX)
transparent code>,与其他
浏览器中的url(#line-gradientXXXXXX)
相比。由于OP没有正确提取 id
,所以ear-marked标记为删除的
linearGradient
标签不是发现而不是
删除,导致他们在数量上增长。
使用数据中已有的唯一索引可以避免这个问题,从而为
linearGradient
标签添加标签。
tick
和 applyGradient
部分的
forEach
/ code>。
d3
来管理 defs
。这可能是好的,它只是花了我一段时间才明白它是如何做的,但是,我将它改为标准的 d3
模式,它将管理更新和正确更改数据。这行是特别敏感的... ...
var new_gradient_id =line-gradient+ getRandomInt();
这个工程更好... ...
var new_gradient_id =lg+ interaction_type + d.source.index + d.target.index;
callLink
和 textLink
CreateVisualizationFromData 。使用这些模式,它会正确更新并管理不断变化的数据。
linearGradient
标签确实存在问题。这些在FF中没有被正确删除,并且大量地膨胀了DOM。我认为这可能是导致这个问题的原因。
HTML
<!DOCTYPE html>
< meta charset =utf-8>
< style>
/ * div {
大纲:1px纯黑色; * /
}
.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;
//笔划:black;
ody {
/ *不同浏览器的缩放比例* /
-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>点击开始/停止< / div>
<! - < script src =d3 / d3 CB.js>< / script> - >
< script type =text / javascriptsrc =jquery.js>< / script>
< script type =text / javascriptsrc =papaparse.js>< / script>
< script type =text / javascriptsrc =networkview CB.js>< / script>
< / body>
var line_diff = 0.5; //如果你想在调用/文本行之间有空格,则从零增加
var mark_offset = 10; //每一端的标记线有多少百分比不用于传入/传出之间的关系?
var mark_size = 5; //行上标记的大小
var legendRectSize = 9; // 18
var legendSpacing = 4; // 4
var recordTypes = [];
var legend;
var text_links_data,call_links_data;
//可视化不同部分的颜色
recordTypes.push({
text:call,
color:#438DCA
});
recordTypes.push({
text:text,
color:#70C05A
});
recordTypes.push({
text:balance,
color:#245A76
});
//用于从数组中获取特定属性的函数
pluck = function(ary,prop){
return ary.map(function(x){
return x [prop]
});
$ sum
sum = function(ary){
return ary.reduce(function(a,b){
return a + b
},0);
$ b maxArray = function(ary){
return ary.reduce(function(a,b){
return Math.max(a,b)
},-Infinity);
$ b 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,
分隔符:,,
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,
分隔符:,,
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);
}
函数node_radius_data(d){
return Math.pow(40.0 * d.size,1/3);
函数dataLoaded(){
if(typeof data_nodes ===undefined|| typeof data_links ===undefined){
console。 log(Still loading+(typeof data_nodes ===undefined?'data_links':'data_nodes'))
} else {
CreateVisualizationFromData();
函数isConnectedToOtherThanMain(a){
var connected = false;如果(isConnected(a,data_nodes [i])&& a.index!= i){$ b $($ i
)(i = 1; i< data_nodes.length; i ++) b connected = true;
}
}
返回连接;
函数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(\"y 2\", 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, $b$ b 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);
}
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
<!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>
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);
}
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
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 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.
forEach
sections in tick
and applyGradient
.d3
to manage the defs
. It was probably fine how it was, it just took me a while to realise how it was done but, I changed it to standard d3
patterns which will manage updating and changing data properly. This line is particularly sensitive...
var new_gradient_id = "line-gradient" + getRandomInt();
this works better...
var new_gradient_id = "lg" + interaction_type + d.source.index + d.target.index;
callLink
and textLink
sections in CreateVisualizationFromData
. Using these patterns it updates properly and manages changing data.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.
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>
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);
}