在D3.V4中从内到外构建一个力导向的同心圆图,其中包含节点排序

时间:2016-10-29 17:22:09

标签: geometry d3.js-v4 d3-force-directed

我目前正在建立D3.js研究论文之间关系的图表。目前我的代码允许我生成一个力导向图。我可以缩放和拖动图表,暂时不会出现这样的问题"丑陋的"工具提示在"鼠标悬停"上显示节点信息(但这与这个问题无关。)

我正在寻找基于发布年度可视化文章网络的最佳方式。我认为最好的方法是按同心圆模式按年显示节点,如下所示:

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

我想我必须修改tick函数,以便在每个&#34; year zone&#34;中返回x和y随机坐标。但不知道如何计算。

关于如何做到这一点的任何想法?非常感谢。

注意:

我发现这个答案是在一个环中生成一个随机数,也就是指在一个圆中统一生成一个随机数:

Generate a uniformly random point within an annulus (ring)

1 个答案:

答案 0 :(得分:1)

我认为有几种方法可以做到这一点,

如下所示的一种方法是约束节点可以移动到的可能位置。我创建了一个logger.log(<LOG_LEVEL>, <LOG_MESSAGE>, ex); 函数,它接受一个节点并更新其x / y以适应由数据集中的年数定义的圆形区域。每次更新节点位置时,只需调用约束函数,它们将保持在其定义的区域内。这样做的一个缺点是边缘力会将它们拉向边界。

&#13;
&#13;
constrain(d)
&#13;
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;
    });
}
&#13;
/* 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;
}
&#13;
&#13;
&#13;