2015-04-17 2 views
0

Я создал силовую схему с использованием D3 (см. Рисунок ниже). Тем не менее, он работает очень медленно в Firefox, в то время как он отлично работает в Chrome. Я отлаживаю его с помощью локального сервера и просматриваю его по адресу http://localhost:8888/. Возможно, это связано с следующим сообщением в консоли Firefox, но в соответствии с комментариями, которые маловероятны. Может ли кто-то определить проблему с производительностью и дать мне подсказку о том, как ее решить?Медленная производительность в Firefox для компоновки силы D3

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

данные и код в zip: https://www.dropbox.com/s/ksh2qk1b5s9lfq5/Network%20View.zip?dl=0

Визуализация:

enter image description here

Index.html

<!DOCTYPE html> 

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

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

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

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

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

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

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

networkview.js

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

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

var text_links_data, call_links_data; 

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

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

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

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

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

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

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

var data_links; 
var data_nodes; 

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

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

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

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

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

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

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

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

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

function tick() { 

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

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

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



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

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

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

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



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

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

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

    var mid_offset = 0; 
    var standardColor = ""; 

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

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

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

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

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

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

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

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

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

var linkedByIndex; 

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

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

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

function CreateVisualizationFromData() { 

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

    linkedByIndex = {}; 

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

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

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

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

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

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

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

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

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


    defs = svg.append("defs"); 

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

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

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

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

    force 
    .on("tick", tick); 

} 

function drawLegend() { 

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

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

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

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

} 

var line_width_factor = 10.0 // width for the widest line 

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

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

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

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

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

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

    return [delta_x, delta_y] 

} 

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

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

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

     return [delta_x, delta_y] 

} 

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

Links.csv

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

Nodes.csv

no_network_info 
0 
0 
0 
1 
1 
0 
0 
0 
0 
0 
0 
1 
0 
1 
0 
0 
0 
1 
0 
1 
1 
0 
0 
0 
0 
1 
0 
0 
0 
0 
1 
0 
1 
0 
1 
1 
0 
0 
0 
0 
1 
1 
0 
0 
1 
0 
0 
0 
0 
0 
+0

Это не ты, это d3. Объекты выбора и перехода являются подклассифицированными массивами, и они подклассифицированы путем изменения члена '__proto__' массива. Это функция, называемая d3_subclass. Я не думаю, что это проблема. –

+0

[Здесь] (https://github.com/mbostock/d3/issues/2191) - это ссылка, объясняющая, почему это сделано. –

+0

Спасибо за объяснение и ссылку - я обновил вопрос :) – pir

ответ

1

РЕДАКТИРОВАТЬ
Основной причиной проблемы был раздувание документа, вызванное отсутствием устаревших linearGradient тегов в разделе defs HTML-кода . Это произошло только в Firefox, из-за чего он возвращает в ответ на getPropertyValue в его CSSStyleDeclaration интерфейс (который вызывается d3 в selection.style()). Возвращаемое значение имеет следующий вид: "url("http://localhost:88888/index.html#line-gradientXXXXXX") transparent", по сравнению с "url(#line-gradientXXXXXX)" в другом браузеров. Так как id не был должным образом извлечен OP, то linearGradient теги, отмеченные ушей для удаления, не были найдены, а не удалены, что приведет к их росту. Проблему можно избежать с помощью , используя уникальную индексацию, уже имеющуюся в данных, для маркировки тегов .

В соответствии с моими комментариями выше, мне удалось решить проблему Firefox, сделав следующие изменения:

  1. Исключите избыточные вычисления в forEach секций в tick и applyGradient.
  2. Использование хорошо сформированного d3 для управления defs. Было, наверное, прекрасно, как это было, мне просто потребовалось некоторое время, чтобы понять, как это было сделано, но я поменял его на стандартные шаблоны d3, которые будут управлять обновлением и изменением данных должным образом. Эта линия особенно чувствительна ...
    var new_gradient_id = "line-gradient" + getRandomInt();
    это работает лучше ...
    var new_gradient_id = "lg" + interaction_type + d.source.index + d.target.index;
  3. Applied стандартных d3 шаблонов для управления разделами callLink и textLink в CreateVisualizationFromData. Используя эти шаблоны, он правильно обновляет и управляет изменением данных.

После внесения этих изменений проблемы с быстродействием в Firefox исчезли, и теперь это то же самое во всех трех основных браузерах с точки зрения скорости. Однако он выглядит лучше в Chrome. Некоторые эксперименты состоят в том, чтобы точно определить, какие изменения имеют решающее значение, но определенно возникла проблема с удалением тегов linearGradient. Они не были должным образом удалены в FF и массово раздувались DOM. Я думаю, что это, вероятно, вызывает проблемы.

Другие изменения, которые я сделал, были просто стилистическими, чтобы облегчить мне понимание.

Измененный код:
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); 
} 
Смежные вопросы