2016-10-29 3 views
0

Ich baue gerade ein Diagramm der Beziehung zwischen Forschungsarbeiten in D3.js. Momentan erlaubt mir mein Code ein kraftgerichtetes Diagramm zu erstellen. Ich kann den Graphen zoomen und ziehen, und vorläufig "hässliche" Tooltips zeigen Knoteninformationen auf "mouseover" an (aber das ist für diese Frage irrelevant).Aufbau eines kraftgerichteten konzentrischen Kreisdiagramms mit Knotenanordnung von innen nach außen in D3.V4

Ich bin auf der Suche nach der besten Möglichkeit zur Visualisierung des Artikels Netzwerk auf der Grundlage des Erscheinungsjahres. Ich glaube, dass der beste Weg, dies zu tun ist, um die Knoten von Jahr in einem konzentrischen Kreismuster wie diese, anzuzeigen:

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

wie in meinem Code Knoten gefärbt ist, basierend auf Jahr im Bild.

Hier ist meine zupfen Link: http://plnkr.co/edit/RCzGe0OFaQNnI32kBuSn?p=preview

Und hier ist mein Code: 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;   
} 

Test-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; 
      } 
     }); 
    } 
}); 

Ich stelle mir vor, dass ich die Zecke Funktion ändern müssen, um x und y zufällige Koordinaten innerhalb jeder „Jahr-Zone“ zurückzukehren, aber nicht wissen, wie diese zu berechnen.

Irgendwelche Ideen, wie man das macht? Danke vielmals.

Hinweis:

ich diese Antwort fand eine Zufallszahl in einem Ring zu erzeugen, die gleichmäßig bezieht sich auch eine Zufallszahl in einem Kreis zu erzeugen:

Generate a uniformly random point within an annulus (ring)

Antwort

1

Ich denke, es gibt ein paar Möglichkeiten, dies zu tun,

Eine Möglichkeit, wie unten gezeigt, ist die Beschränkung der möglichen Standorte, die die Knoten verschieben können. Ich habe eine constrain(d)-Funktion erstellt, die einen Knoten aufnimmt und sein X/Y so aktualisiert, dass es in einen kreisförmigen Bereich passt, der durch die Anzahl der Jahre im Dataset definiert wird. Jedes Mal, wenn die Knotenpositionen aktualisiert werden, rufen Sie einfach die Constrain-Funktion auf und sie bleiben innerhalb ihrer definierten Bereiche. Ein Nachteil davon ist, dass die Kantenkräfte dazu neigen, sie zu den Grenzen zu ziehen.

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

Dies ist in der Tat die richtige Antwort, hatte ich das gleiche gefunden. Allerdings würde ich gerne Erklärungen zu den Gründen für die Funktion constrain für diejenigen hinzufügen, die nicht mit D3.js oder der Mathematik vertraut sind. Und warum kommentieren Leute ihren Code nicht? – David