2016-10-29 36 views
0

我目前正在構建D3.js中研究論文之間關係的圖。目前我的代碼允許我生成一個強制定向圖。我可以放大和拖動圖形,暫時「醜陋」的工具提示顯示關於「鼠標懸停」的節點信息(但這與此問題無關)。在D3.V4中構建一個節點排序的力指向同心圓圖.V4

我在尋找最佳的方式來根據出版年來可視化文章網絡。我認爲,要做到這一點,最好的辦法是在同心圓圖案顯示由一年的節點,就像這樣:

Simple representation of the expected result of a concentric circle force-directed graph

在像在我的代碼節點基於一年的彩色圖像。

這裏是我的普拉克鏈接:http://plnkr.co/edit/RCzGe0OFaQNnI32kBuSn?p=preview

這裏是我的代碼: HTML:

<!DOCTYPE html> 
<html> 

    <head> 
    <script src="https://d3js.org/d3.v4.min.js"></script> 
    <link rel="stylesheet" href="style.css"> 
    </head> 
    <body> 
    <script src="script.js"></script> 
    </body> 

</html> 

的style.css:

/* Styles go here */ 

.links line { 
    stroke: #999; 
    stroke-opacity: 0.6; 
} 

.nodes circle { 
    stroke: #fff; 
    stroke-width: 1.5px; 
} 

div.tooltip { 
    position: absolute;   
    text-align: center;    
    padding: 2px;    
    font: 12px sans-serif;   
    background: lightsteelblue; 
    border: 0px;  
    border-radius: 8px;   
    pointer-events: none;   
} 

測試data.JSON:

{ 
    "papers":[ 
    { 
     "id":"1", 
     "title":"Title 1", 
     "year":"2016", 
     "authors":["A1","A2"], 
     "problematic":"", 
     "solution":"", 
     "references":["2","3"] 
     }, 
    { 
     "id":"2", 
     "title":"Title 2", 
     "year":"2015", 
     "authors":["A2","A3"], 
     "problematic":"", 
     "solution":"", 
     "references":["4","5"] 
     }, 
    { 
     "id":"3", 
     "title":"Title 3", 
     "year":"2015", 
     "authors":["A4","A5"], 
     "problematic":"", 
     "solution":"", 
     "references":["4"] 
     }, 
    { 
     "id":"4", 
     "title":"Title 4", 
     "year":"2014", 
     "authors":["A1","A3"], 
     "problematic":"", 
     "solution":"", 
     "references":[] 
     }, 
    { 
     "id":"5", 
     "title":"Title 5", 
     "year":"2013", 
     "authors":["A6","A7"], 
     "problematic":"", 
     "solution":"", 
     "references":[] 
     } 
    ] 
} 

的script.js:

/* ------ DESCRIPTION ------ 
    Properties of the graph: 
    BASIC: 
    ✓ Graph represents all papers and relationships in RTB research 
    ✓ Graph is force dynamic 
    ✓ Nodes are coloured by publishing year 
    ✓ Graph is draggable 
    ✓ Graph is zoomable 
    X Graph is "tree like" where the nodes are "ordered" by publishing year, the oldest being at the bottom 
    ~ Hovering over a Node will display it's info 
    - Clicking a node will allow to visualize it's direct or most important connections 

    ADVANCED: 
    - Display papers graph 
    - Display authors graph 
    - Search for paper based on info: id, title, author, year, ... 
    - Add new paper to graph and modify and save JSON file 
    - Open PDF File in new Tab 
*/ 


// ----- GLOBAL VARIABLES ------ 
var w = window.innerWidth; 
var h = window.innerHeight; 

var svg = d3.select("body").append("svg") 
          .attr("width",w) 
          .attr("height",h) 
          .style("cursor","move"); 
var g = svg.append("g"); 

// NODE COLORS 
var color = d3.scaleOrdinal(d3.schemeCategory20); 


// FORCE SIMULATION 

var simulation = d3.forceSimulation() 
        .force("link", d3.forceLink().id(function(d) { return d.id; })) 
        .force("charge", d3.forceManyBody().strength(-100)) 
        .force("center", d3.forceCenter(w/2, h/2)) 
        .force("collide", d3.forceCollide(10)); 

// ZOOM PARAMETERS 
var min_zoom = 0.1; 
var max_zoom = 7; 
var zoom = d3.zoom() 
       .scaleExtent([min_zoom,max_zoom]) 
       .on("zoom", zoomed); 
svg.call(zoom); 
var transform = d3.zoomIdentity 
        .translate(w/6, h/6) 
        .scale(0.5); 

svg.call(zoom.transform, transform); 

// BASIC NODE SIZE 
var nominal_stroke = 1.5; 
var nominal_node_size = 8; 

// ----- GLOBAL FUNCTIONS ----- 

function dragStart(d){ 
    if (!d3.event.active) simulation.alphaTarget(0.3).restart(); 
    d.fx = d.x; 
    d.fy = d.y; 
} 

function dragging(d){ 
    d.fx = d3.event.x; 
    d.fy = d3.event.y; 
} 

function dragEnd(d){ 
    if (!d3.event.active) simulation.alphaTarget(0); 
    d.fx = null; 
    d.fy = null; 
} 

function zoomed() { 
    g.attr("transform", d3.event.transform); 
    // Manually offsets the zoom to compensate for the initial position. Should get fixed asap or the position variables made global. 
    //svg.attr("transform", "translate(" + (d3.event.transform.x + 400) + "," + (d3.event.transform.y + 325) + ")scale(" + d3.event.transform.k + ")"); 
} 

function isInList(el, list){ 
    for (var i = 0; i < list.length; i++){ 
    if (el == list[i]) return true; 
    } 
    return false; 
} 

// builds a graph dictionary based on paper references 
function referencesGraph(file_data){ 
    var nodes = []; 
    var links = []; 

    // we use these to add nodes to references that are missing as nodes 
    var node_ids = []; 
    var ref_ids = []; 

    // for each paper in graph create a node and append result to node list 
    for (var i = 0; i < file_data.length; i++){ 
    var node = { 
     "id":file_data[i].id, 
     "title":file_data[i].title, 
     "year":file_data[i].year, 
     "authors":file_data[i].authors 
    }; 

    node_ids.push(file_data[i].id); 
    nodes.push(node); 

    // for each referenced paper in graph create a link and append result to link list 
    for (var j = 0; j < file_data[i].references.length; j++){ 
     var link = { 
     "source":file_data[i].id, 
     "target":file_data[i].references[j] 
     }; 

     ref_ids.push(file_data[i].references[j]); 
     links.push(link); 
    } 
    } 

    //check if all referenced elements have a node associated 
    for (var i = 0; i < ref_ids.length; i++){ 
    if (!isInList(ref_ids[i],node_ids)){ 
     var node = { 
     "id":ref_ids[i], 
     "title":ref_ids[i], 
     "year":"" 
     } 

     nodes.push(node); 
    } 
    } 

    var graph = { 
    "nodes":nodes, 
    "links":links 
    }; 
    return graph; 
} 

// builds a graph dictionary based on author collaboration 
function authorsGraph(data){ 

} 

// DEAL WITH MISSING DATA TO BE WORKED 

// ----- MANAGE JSON DATA ----- 
d3.json("test-data.json",function(error,graph){ 
    if (error) throw error; 

    // Read the JSON data and create a dictionary of nodes and links based on references 
    var paper_graph_data = referencesGraph(graph.papers); 

    //var authors_graph_data; //function not implemented yet 

    // INITIALIZE THE LINKS 
    var link = g.append("g") 
       .attr("class","links") 
       .selectAll("line") 
       .data(paper_graph_data.links) 
       .enter() 
       .append("line") 
       .attr("stroke-width",function(d){return nominal_stroke}) 

    /* FUNCTION THAT CREATES DIV ELEMENT TO HOLD NODE INFORMATION 
    [    PAPER TITLE    ] 
    [ PUBLISHING YEAR ][ PERSONAL RATING ] 
    [   AUTHORS & LINKS    ] 
    [    PROBLEMATIC    ] 
    [    SOLUTION     ] 
           [OPEN PDF FILE] 
    */ 
    var div = d3.select("body").append("div") 
          .attr("class", "tooltip")    
          .style("opacity", 0); 

    function createTooltip(d){ 
    //get node data, manage missing values 
    div.transition()   
     .duration(200)  
     .style("opacity", .9); 

    div.html("<table><tr><td>" + d.title + "</td></tr><tr><td>" + d.year + "</td></tr><tr><td>" + d.authors + "</td></tr><tr><td>" + d.problematic + "</td></tr><tr><td>" + d. solution + "</td></tr></table>") 
     .style("left", (d3.event.pageX) + "px")  
     .style("top", (d3.event.pageY - 28) + "px"); 
    } 

    // INITIALIZE THE NODES 
    var node = g.append("g") 
       .attr("class","nodes") 
       .selectAll("circles") 
       .data(paper_graph_data.nodes) 
       .enter() 
       .append("circle") 
       .attr("r",nominal_node_size) 
       .attr("fill",function(d){return color(d.year);}) 
       .style("cursor","pointer") 
       .on("mouseover",createTooltip) 
       .on("mouseout",function(d){ 
        div.transition()   
        .duration(500)  
        .style("opacity", 0); 
       }) 
       .call(d3.drag() 
         .on("start", dragStart) 
         .on("drag", dragging) 
         .on("end", dragEnd)); 

    simulation.nodes(paper_graph_data.nodes) 
      .on("tick",ticked); 

    simulation.force("link") 
      .links(paper_graph_data.links); 

    // function to return link and node position when simulation is generated 
    function ticked(){ 
    // Each year is placed on a different level to get chronological order of paper network 
    /* 
    switch(d.source.year){ 
      case "2016": 
       return 40; 
      case "2015": 
       return 80; 
      case "2014": 
       return 120; 
      case "2013": 
       return 160; 
      case "2012": 
       return 200; 
      case "2011": 
       return 240; 
      case "2010": 
       return 280; 
      case "2009": 
       return 320; 
      case "2008": 
       return 360; 
      case "2007": 
       return 400; 
      default: 
       return 600; 
      } 
    */ 

    link 
     .attr("x1", function(d) { return d.source.x; }) 
     .attr("y1", function(d) { return d.source.y; }) 
     .attr("x2", function(d) { return d.target.x; }) 
     .attr("y2", function(d) { return d.target.y; }); 

    node 
     .attr("cx", function(d) { return d.x; }) 
     .attr("cy", function(d) { return d.y; }); 
    } 

    function ticked_advanced(){ 
    link 
     .attr("x1", function(d) { return d.source.x; }) 
     .attr("y1", function(d) { 
      switch(d.source.year){ 
      case "2016": 
       return 40; 
      case "2015": 
       return 80; 
      case "2014": 
       return 120; 
      case "2013": 
       return 160; 
      case "2012": 
       return 200; 
      case "2011": 
       return 240; 
      case "2010": 
       return 280; 
      case "2009": 
       return 320; 
      case "2008": 
       return 360; 
      case "2007": 
       return 400; 
      default: 
       return 600; 
      } 
     }) 
     .attr("x2", function(d) { return d.target.x; }) 
     .attr("y2", function(d) { 
      switch(d.target.year){ 
      case "2016": 
       return 40; 
      case "2015": 
       return 80; 
      case "2014": 
       return 120; 
      case "2013": 
       return 160; 
      case "2012": 
       return 200; 
      case "2011": 
       return 240; 
      case "2010": 
       return 280; 
      case "2009": 
       return 320; 
      case "2008": 
       return 360; 
      case "2007": 
       return 400; 
      default: 
       return 600; 
      } 
     }); 

    node 
     .attr("cx", function(d) { return d.x; }) 
     .attr("cy", function(d) { 
      switch(d.year){ 
      case "2016": 
       return 40; 
      case "2015": 
       return 80; 
      case "2014": 
       return 120; 
      case "2013": 
       return 160; 
      case "2012": 
       return 200; 
      case "2011": 
       return 240; 
      case "2010": 
       return 280; 
      case "2009": 
       return 320; 
      case "2008": 
       return 360; 
      case "2007": 
       return 400; 
      default: 
       return 600; 
      } 
     }); 
    } 
}); 

我想,我必須修改刻度功能,以每個「年度區」內返回x和y座標隨機,但不知道如何計算這一點。

有關如何做到這一點的任何想法?非常感謝。

注:

我發現這個答案,生成的環也指了一圈均勻的產生隨機數的隨機數:

Generate a uniformly random point within an annulus (ring)

回答

1

我覺得有幾個如何做到這一點,

下面顯示的一種方法是約束節點可以移動到的可能位置。我創建了一個constrain(d)函數,該函數接受一個節點並更新其x/y以適合由數據集中的年數定義的圓形區域。任何時候節點位置都被更新,只需調用約束函數,它們將保持在其定義的區域內。這樣做的一個缺點是邊緣力量會傾向於將它們拉向邊界。

var graph = { 
 
    "papers": [{ 
 
    "id": "1", 
 
    "title": "Title 1", 
 
    "year": "2016", 
 
    "authors": ["A1", "A2"], 
 
    "problematic": "", 
 
    "solution": "", 
 
    "references": ["2", "3"] 
 
    }, { 
 
    "id": "2", 
 
    "title": "Title 2", 
 
    "year": "2015", 
 
    "authors": ["A2", "A3"], 
 
    "problematic": "", 
 
    "solution": "", 
 
    "references": ["4", "5"] 
 
    }, { 
 
    "id": "3", 
 
    "title": "Title 3", 
 
    "year": "2015", 
 
    "authors": ["A4", "A5"], 
 
    "problematic": "", 
 
    "solution": "", 
 
    "references": ["4"] 
 
    }, { 
 
    "id": "4", 
 
    "title": "Title 4", 
 
    "year": "2014", 
 
    "authors": ["A1", "A3"], 
 
    "problematic": "", 
 
    "solution": "", 
 
    "references": [] 
 
    }, { 
 
    "id": "5", 
 
    "title": "Title 5", 
 
    "year": "2013", 
 
    "authors": ["A6", "A7"], 
 
    "problematic": "", 
 
    "solution": "", 
 
    "references": [] 
 
    }] 
 
}; 
 

 

 
var w = window.innerWidth; 
 
var h = window.innerHeight; 
 
var maxRadStep = 100; 
 
var cX = w/2, 
 
    cY = h/2; 
 

 
var years = d3.set(graph.papers.map(function(obj) { 
 
    return +obj.year; 
 
})).values(); 
 
years.sort(); 
 

 
function constrain(d) { 
 
    var yearIndex = years.indexOf(d.year); 
 
    var max = (maxRadStep * (yearIndex + 1)) - 10; 
 
    var min = (max - maxRadStep) + 20; 
 
    var vX = d.x - cX; 
 
    var vY = d.y - cY; 
 
    var magV = Math.sqrt(vX * vX + vY * vY); 
 
    if (magV > max) { 
 
    d.vx = 0; 
 
    d.vy = 0; 
 
    d.x = cX + vX/magV * max; 
 
    d.y = cY + vY/magV * max; 
 
    } else if (magV < min) { 
 
    d.vx = 0; 
 
    d.vy = 0; 
 
    d.x = cX + vX/magV * min; 
 
    d.y = cY + vY/magV * min; 
 
    } 
 
} 
 

 
var svg = d3.select("body").append("svg") 
 
    .attr("width", w) 
 
    .attr("height", h) 
 
    .style("cursor", "move"); 
 
var g = svg.append("g"); 
 

 
// NODE COLORS 
 
var color = d3.scaleOrdinal(d3.schemeCategory20); 
 

 

 
// FORCE SIMULATION 
 

 
var simulation = d3.forceSimulation() 
 
    .force("link", d3.forceLink().id(function(d) { 
 
    return d.id; 
 
    })) 
 
    .force("charge", d3.forceManyBody().strength(-100)) 
 
    //.force("center", d3.forceCenter(w/2, h/2)) 
 
    .force("collide", d3.forceCollide(10)); 
 

 
// ZOOM PARAMETERS 
 
var min_zoom = 0.1; 
 
var max_zoom = 7; 
 
var zoom = d3.zoom() 
 
    .scaleExtent([min_zoom, max_zoom]) 
 
    .on("zoom", zoomed); 
 
svg.call(zoom); 
 
var transform = d3.zoomIdentity 
 
    .translate(w/6, h/6) 
 
    .scale(0.5); 
 

 
svg.call(zoom.transform, transform); 
 

 
// BASIC NODE SIZE 
 
var nominal_stroke = 1.5; 
 
var nominal_node_size = 8; 
 

 
// ----- GLOBAL FUNCTIONS ----- 
 

 
function dragStart(d) { 
 
    if (!d3.event.active) simulation.alphaTarget(0.3).restart(); 
 
    d.fx = d.x; 
 
    d.fy = d.y; 
 
} 
 

 
function dragging(d) { 
 
    console.log(d3.event.x + ' ' + d3.event.y); 
 
    d.fx = d3.event.x; 
 
    d.fy = d3.event.y; 
 
    constrain(d); 
 
} 
 

 
function dragEnd(d) { 
 
    if (!d3.event.active) simulation.alphaTarget(0); 
 
    d.fx = null; 
 
    d.fy = null; 
 
} 
 

 
function zoomed() { 
 
    g.attr("transform", d3.event.transform); 
 
    // Manually offsets the zoom to compensate for the initial position. Should get fixed asap or the position variables made global. 
 
    //svg.attr("transform", "translate(" + (d3.event.transform.x + 400) + "," + (d3.event.transform.y + 325) + ")scale(" + d3.event.transform.k + ")"); 
 
} 
 

 
function isInList(el, list) { 
 
    for (var i = 0; i < list.length; i++) { 
 
    if (el == list[i]) return true; 
 
    } 
 
    return false; 
 
} 
 

 
// builds a graph dictionary based on paper references 
 
function referencesGraph(file_data) { 
 
    var nodes = []; 
 
    var links = []; 
 

 
    // we use these to add nodes to references that are missing as nodes 
 
    var node_ids = []; 
 
    var ref_ids = []; 
 

 
    // for each paper in graph create a node and append result to node list 
 
    for (var i = 0; i < file_data.length; i++) { 
 
    var node = { 
 
     "id": file_data[i].id, 
 
     "title": file_data[i].title, 
 
     "year": file_data[i].year, 
 
     "authors": file_data[i].authors 
 
    }; 
 

 
    node_ids.push(file_data[i].id); 
 
    nodes.push(node); 
 

 
    // for each referenced paper in graph create a link and append result to link list 
 
    for (var j = 0; j < file_data[i].references.length; j++) { 
 
     var link = { 
 
     "source": file_data[i].id, 
 
     "target": file_data[i].references[j] 
 
     }; 
 

 
     ref_ids.push(file_data[i].references[j]); 
 
     links.push(link); 
 
    } 
 
    } 
 

 
    //check if all referenced elements have a node associated 
 
    for (var i = 0; i < ref_ids.length; i++) { 
 
    if (!isInList(ref_ids[i], node_ids)) { 
 
     var node = { 
 
     "id": ref_ids[i], 
 
     "title": ref_ids[i], 
 
     "year": "" 
 
     } 
 

 
     nodes.push(node); 
 
    } 
 
    } 
 

 
    var graph = { 
 
    "nodes": nodes, 
 
    "links": links 
 
    }; 
 
    return graph; 
 
} 
 

 
// builds a graph dictionary based on author collaboration 
 
function authorsGraph(data) { 
 

 
} 
 

 
// DEAL WITH MISSING DATA TO BE WORKED 
 

 
// ----- MANAGE JSON DATA ----- 
 

 
// Read the JSON data and create a dictionary of nodes and links based on references 
 
var paper_graph_data = referencesGraph(graph.papers); 
 

 
//var authors_graph_data; //function not implemented yet 
 

 
// INITIALIZE THE LINKS 
 
var link = g.append("g") 
 
    .attr("class", "links") 
 
    .selectAll("line") 
 
    .data(paper_graph_data.links) 
 
    .enter() 
 
    .append("line") 
 
    .attr("stroke-width", function(d) { 
 
    return nominal_stroke 
 
    }) 
 

 
// INITIALIZE THE NODES 
 
var node = g.append("g") 
 
    .attr("class", "nodes") 
 
    .selectAll("circles") 
 
    .data(paper_graph_data.nodes) 
 
    .enter() 
 
    .append("circle") 
 
    .attr("r", nominal_node_size) 
 
    .attr("fill", function(d) { 
 
    return color(d.year); 
 
    }) 
 
    .style("cursor", "pointer") 
 
    .call(d3.drag() 
 
    .on("start", dragStart) 
 
    .on("drag", dragging) 
 
    .on("end", dragEnd)); 
 

 
g.append('g') 
 
    .attr('class', 'boundry') 
 
    .selectAll('.boundry') 
 
    .data(years) 
 
    .enter() 
 
    .append('circle') 
 
    .attr('r', function(d, index) { 
 
    return (index + 1) * maxRadStep; 
 
    }).attr('cx', cX).attr('cy', cY); 
 

 
simulation.nodes(paper_graph_data.nodes) 
 
    .on("tick", ticked); 
 

 
simulation.force("link") 
 
    .links(paper_graph_data.links); 
 

 
function ticked() { 
 
    node.each(constrain); 
 
    node 
 
    .attr("cx", function(d) { 
 
     return d.x; 
 
    }) 
 
    .attr("cy", function(d) { 
 
     return d.y; 
 
    }); 
 
    link 
 
    .attr("x1", function(d) { 
 
     return d.source.x; 
 
    }) 
 
    .attr("y1", function(d) { 
 
     return d.source.y; 
 
    }) 
 
    .attr("x2", function(d) { 
 
     return d.target.x; 
 
    }) 
 
    .attr("y2", function(d) { 
 
     return d.target.y; 
 
    }); 
 
}
/* Styles go here */ 
 

 
.links line { 
 
    stroke: #999; 
 
    stroke-opacity: 0.6; 
 
} 
 

 
.nodes circle { 
 
    stroke: #fff; 
 
    stroke-width: 1.5px; 
 
} 
 

 
.boundry circle { 
 
    stroke: #000; 
 
    fill: none; 
 
}
<script src="https://d3js.org/d3.v4.min.js"></script>

+0

這確實是正確的答案,我也發現了同樣的。不過,我想補充說明那些不熟悉D3.js或數學的人對函數約束背後的推理。爲什麼人們不會評論他們的代碼? – David