用d3或cytoscape渲染家谱 [英] Rendering a family tree with d3 or cytoscape

查看:64
本文介绍了用d3或cytoscape渲染家谱的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我在用JavaScript生成美观的家谱时遇到了问题.

要求:

  • 每个孩子应该与树中的两个父母联系在一起,而不是像某些图中的一个父母
  • 我希望配偶在树上彼此相邻(垂直位置相同)
  • 我希望按垂直方向组织节点,以便您一眼就能看到同一十年内出生的人.
  • 一个人随时间推移可以有多个配偶,每个孩子都有孩子
  • 可以在树上自由添加父母和孩子,所以不仅仅是追溯一个人的血统"

我尝试过的最接近的结果:

  1. (带有随机数据的图表.实线是父子关系,虚线是配偶)

    问题在于配偶彼此之间没有对齐.Dagre历史上一直支持将"rank"作为节点的参数,这意味着您可以强制某些节点处于特定的高度(如果愿意,可以将其视为世代").不幸的是,它

我的问题

是否可以像我上面描述的那样垂直对齐我的cytoscape/dagre图中的节点?

如果没有,我愿意尝试其他库和其他布局算法.

我正在寻找一个与yFiles解决方案相似但使用开源工具的工作示例.

解决方案

在深入了解我的答案 :)之前,您可能想看看指定x和y尺寸约束为我在下面的示例中完成了y维度.我自己没有使用过它,但是看起来非常适合您的要求.而且它可以与CytoScape一起使用,因此您可能可以在已经完成的工作的基础上...

将尺寸约束应用于力导向图:

由于您没有处理严格的等级结构(例如,您不是从一个后代开始而是逐步向上),因此一种方法是使用 D3强制有向图,带有一个代表每个家庭成员的节点.与线性层次结构相比,这将提供更多的灵活性.

您可以通过将节点约束到y轴上的固定点来实现所需的世代布局.

这是概念证明:

  • 三代家庭成员
  • 爱丽丝(Alice)和鲍勃(Bob)/鲍勃(Bob)和卡罗尔(Carol)代表多个配偶.
  • David是爱丽丝和鲍勃的孩子
  • James是Bob和Carol的孩子
  • assignGeneration 根据链接的子节点,伙伴节点和父节点计算的
  • 节点生成(或y坐标)
  • 节点X坐标由d3处理,我认为它比尝试手动在x轴上为每个节点分配位置要更可靠
  • 基本样式:
    • 合作伙伴链接是珊瑚
    • 子链接为浅蓝色
    • 同级链接为浅绿色

希望这里有足够的资源来决定这是否可行.在父母和孩子之间建立演示性的纵向/横向链接应该很简单,但可能需要一些实验.

调整(取决于数据量和节点关系等)可能需要应用于 simulation -同样,将需要进行一些实验才能生成最佳布局.此处.

 <!DOCTYPE html>< html>< head>< style>svg {边框:1px纯灰色;}.partner_link {中风:淡珊瑚色;}.child_link {中风:浅蓝色;}.sibling_link {中风:浅海绿色;}</style></head>< body>< script src ="https://d3js.org/d3.v5.min.js"></script>< script type ="text/javascript">var nodeData = [{编号:1名称:爱丽丝",合作伙伴:[2],儿童:[4]},{编号:2名称:鲍勃",合作伙伴:[1,3],儿童:[4,10]},{id:3,名称:"Carol",合作伙伴:[2],儿童:[10]},{编号:4名称:"David",合作伙伴:[7],儿童:[8]},{编号:5名称:艾米丽",合作伙伴:[6],儿童:[7、9]},{编号:6名称:"Fred",合作伙伴:[5],儿童:[7、9]},{编号:7名称:宽限期",合作伙伴:[4],儿童:[8]},{编号:8名称:"Harry",合作伙伴:null,子代:null},{编号:9名称:"Imogen",合作伙伴:null,子代:null},{编号:10,名称:詹姆斯",合作伙伴:null,子代:null}];var linkData = [];nodeData.forEach((node,index)=> {如果(node.partners){node.partners.forEach(partnerID => {linkData.push({源:节点,目标:nodeData.find(partnerNode => partnerNode.id === partnerID),关系:'Partner'});})}如果(node.children){node.children.forEach(childID => {const childNode = nodeData.find(childNode => childNode.id === childID);如果(node.children.length> 1){childNode.siblings = node.children.slice(0,node.children.indexOf(childNode.id)).concat(node.children.slice(node.children.indexOf(childNode.id)+ 1,node.children.length));childNode.siblings.forEach(siblingID => {linkData.push({源:childNode,目标:nodeData.find(siblingNode => siblingNode.id === siblingID),关系:'Sibling'});})}linkData.push({源:节点,目标:childNode,关系:'Child'});})}});linkData.map(d => Object.create(d));AssignGeneration(nodeData,nodeData,0);var w = 500,h = 500;var svg = d3.select("body").append("svg").attr("width",w).attr("height",h);var color = d3.scaleOrdinal(d3.schemeCategory10);var rowScale = d3.scalePoint().domain(dataRange(nodeData,'generation')).range([0,h-50]).padding(0.5);var模拟= d3.forceSimulation(nodeData).force('link',d3.forceLink().links(linkData).distance(50).strength(1)).force("y",d3.forceY(function(d){返回rowScale(d.generation)})).force("charge",d3.forceManyBody().strength(-300).distanceMin(60).distanceMax(120)).force("center",d3.forceCenter(w/2,h/2));var links = svg.append("g").attr("stroke",#999").attr("stroke-opacity",0.8).selectAll("line").data(linkData).join("line").attr("stroke-width",1).attr("class",d => {返回d.relationship.toLowerCase()+'_link';});;var个节点= svg.append("g").attr("class","nodes").selectAll("g").data(nodeData).enter().append("g")var circle = nodes.append("circle").attr("r",5).attr("fill",function(d){返回颜色(d.generation)});var nodeLabels = nodes.append("text").text(function(d){返回d.name;}).attr('x',12).attr('y',20);var linkLabels = links.append("text").text(function(d){d.关系}).attr('x',12).attr('y',20);/*//Y轴-适用于测试:var yAxis = d3.axisLeft(rowScale)(svg.append("g").attr("transform","translate(30,0)")));*/Simulation.on("tick",function(){链接.attr("x1",d => {返回d.source.x;}).attr("y1",d => {返回rowScale(d.source.generation);}).attr("x2",d => {返回d.target.x;}).attr("y2",d => {返回rowScale(d.target.generation);});nodes.attr("transform",函数(d){返回"translate(" + d.x +," + rowScale(d.generation)+)";})});函数dataRange(records,field){var min = d3.min(records.map(record => parseInt(record [field],10)));var max = d3.max(records.map(record => parseInt(record [field],10))));返回d3.range(min,max + 1);};函数AssignGeneration(nodes,generationNodes,generationCount){const childNodes = [];generationNodes.forEach(功能(节点){如果(node.children){//节点有孩子node.generation = generationCount + 1;node.children.forEach(childID => {if(!childNodes.find(childNode => gt childNode.id === childID)){childNodes.push(generationNodes.find(childNode => childNode.id === childID));}})} 别的 {如果(node.partners){node.partners.forEach(partnerID => {如果(generationNodes.find(partnerNode => partnerNode.id === partnerID&& partnerNode.children)){//Node与孩子有伙伴node.generation = generationCount + 1;}})} 别的 {//使用父代+ 1的生成const parent = nodes.find(parentNode => parentNode.children&& parentNode.children.indexOf(node.id)!== -1);node.generation = parent.generation + 1;}}});如果(childNodes.length> 0){返回assignGeneration(nodes,childNodes,generationCount + = 1);} 别的 {node.filter(node =>!node.generation).forEach(function(node){node.generation = generationCount + 1;});返回节点;}}</script></body></html>  

I'm having problems generating a good looking family tree with Javascript.

Requirements:

  • Each child should be connected to two parents in the tree, not one as in some graphs
  • I've like spouses to be next to each other in the tree (same vertical position)
  • I'd like to organize the nodes by generation vertically, so that you can see people born in the same decade at a glance.
  • One person can have multiple spouses over time, and children with each of them
  • Parents and children can be added freely in the tree, so not just "trace the lineage from one person upwards"

What I've tried that has come closest to this:

  1. Cytoscape JS with Dagre as layout engine, and curve-style: taxi edges enabled.

    (Chart with random data. Solid lines are parent-child relationsships, dashed lines are spouses)

    The problem is that spouses are not aligned with each other. Dagre has historically supported "rank" as a parameter for nodes, which means that you could force some nodes to be at a specific height (think of it as a "generation" if you will). Unfortunately, it doesn't work any longer, and the responsible developer does not work on the project any longer. This would nicely solve my problem.

Other things I've tried but failed:

  1. Downgrading dagre to an older version that has support for rank?

    Haven't gotten rank to work with ANY version of dagre.

  2. D3 with dagre-d3

    Same problem as above, as dagre-d3 is a modified version of dagre, which means it does not have support for ranking in generations.

  3. yFiles family tree demo looks great, but is commercial. The cost for my purposes (would like anyone to set up their own family tree) is 26.000 USD (!?!) for a single developer license. Obviously not acceptable.

My question

Is it possible to get align the nodes in my cytoscape/dagre graph vertically as I've described above?

If not, i'm open to trying other libraries and other layout algorithms.

I'm looking for a working example that looks similar to the yFiles solution, but using open source tools.

解决方案

Before you get too deep into my answer :) you might want to check out WebCola, which I came across while researching constrained force directed graphs:

JavaScript constraint based layout for high-quality graph visualization and exploration using D3.js and other web-based graphics libraries.

It allows you to specify x and y dimensional constraints as I've done on the y dimension in my example below. I've not used it myself, but looks like a really good fit for your requirements. And it works with CytoScape so you may be able to build on what you've already done...

Applying dimensional constraints to a force-directed graph:

As you're not dealing with a strict hierarchy (e.g. you're not starting with one descendant and working your way up), one approach would be to use a D3 Force Directed Graph with a node to represent each family member. This will provide added flexibility, compared with a linear hierarchy.

The generational layout you're looking for could then be achieved by constraining the nodes to fixed points on the y axis.

Here is a proof of concept:

  • Three generations of family members
  • Multiple spouses are represented by Alice and Bob / Bob and Carol
  • David is a child of Alice and Bob
  • James is a child of Bob and Carol
  • Node generation (or y coordinate) calculated by assignGeneration based on linked child, partner and parent nodes
  • Node X coordinate is handled by d3, which I think will be more robust than trying to manually assign each node a position on the x axis
  • Basic styling:
    • Partner links are coral
    • Child links are light blue
    • Sibling links are light green

Hopefully there is enough here for you to decide whether this is a viable approach. Setting up presentational vertical / horizontal links between parents and children should be fairly straightforward but might take a bit of experimentation.

Adjustments (depending on the volume of data and node relationships etc.) will likely need to be applied to simulation - again, some experimentation will be needed to generate an optimal layout. More information about the different forces available here.

<!DOCTYPE html>
<html>

<head>
  <style>
svg {
  border: 1px solid gray;
}

.partner_link {
  stroke: lightcoral;
}

.child_link {
  stroke: lightskyblue;
}

.sibling_link {
  stroke: lightseagreen;
}
  </style>
</head>

<body>
  <script src="https://d3js.org/d3.v5.min.js"></script>
  <script type="text/javascript">

var nodeData = [{
  id: 1,
  name: 'Alice',
  partners: [2],
  children: [4]
}, {
  id: 2,
  name: 'Bob',
  partners: [1, 3],
  children: [4,10]
}, {
  id: 3,
  name: 'Carol',
  partners: [2],
  children: [10]
}, {
  id: 4,
  name: 'David',
  partners: [7],
  children: [8]
}, {
  id: 5,
  name: 'Emily',
  partners: [6],
  children: [7, 9]
}, {
  id: 6,
  name: 'Fred',
  partners: [5],
  children: [7, 9]
}, {
  id: 7,
  name: 'Grace',
  partners: [4],
  children: [8]
}, {
  id: 8,
  name: 'Harry',
  partners: null,
  children: null
}, {
  id: 9,
  name: 'Imogen',
  partners: null,
  children: null
}, {
  id: 10,
  name: 'James',
  partners: null,
  children: null
}];

var linkData = [];

nodeData.forEach((node, index) => {
  if (node.partners) {
    node.partners.forEach(partnerID => {
      linkData.push({ source: node, target: nodeData.find(partnerNode => partnerNode.id === partnerID), relationship: 'Partner' });
    })
  }
  if (node.children) {
    node.children.forEach(childID => {
      const childNode = nodeData.find(childNode => childNode.id === childID);
      if (node.children.length > 1) {
        childNode.siblings = node.children.slice(0, node.children.indexOf(childNode.id)).concat(node.children.slice(node.children.indexOf(childNode.id) + 1, node.children.length));
        childNode.siblings.forEach(siblingID => {
          linkData.push({ source: childNode, target: nodeData.find(siblingNode => siblingNode.id === siblingID), relationship: 'Sibling' });
        })
      }
      linkData.push({ source: node, target: childNode, relationship: 'Child' });
    })
  }
});

linkData.map(d => Object.create(d));

assignGeneration(nodeData, nodeData, 0);

var w = 500,
  h = 500;

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

var color = d3.scaleOrdinal(d3.schemeCategory10);

var rowScale = d3.scalePoint()
  .domain(dataRange(nodeData, 'generation'))
  .range([0, h - 50])
  .padding(0.5);

var simulation = d3.forceSimulation(nodeData)
  .force('link', d3.forceLink().links(linkData).distance(50).strength(1))
  .force("y", d3.forceY(function (d) {
    return rowScale(d.generation)
  }))
  .force("charge", d3.forceManyBody().strength(-300).distanceMin(60).distanceMax(120))
  .force("center", d3.forceCenter(w / 2, h / 2));

var links = svg.append("g")
  .attr("stroke", "#999")
  .attr("stroke-opacity", 0.8)
  .selectAll("line")
  .data(linkData)
  .join("line")
  .attr("stroke-width", 1)
  .attr("class", d => {
    return d.relationship.toLowerCase() + '_link';
  });;

var nodes = svg.append("g")
  .attr("class", "nodes")
  .selectAll("g")
  .data(nodeData)
  .enter().append("g")

var circles = nodes.append("circle")
  .attr("r", 5)
  .attr("fill", function (d) {
    return color(d.generation)
  });

var nodeLabels = nodes.append("text")
  .text(function (d) {
    return d.name;
  }).attr('x', 12)
  .attr('y', 20);

var linkLabels = links.append("text")
  .text(function (d) {
    return d.relationship;
  }).attr('x', 12)
  .attr('y', 20);

/*
// Y Axis - useful for testing:
var yAxis = d3.axisLeft(rowScale)(svg.append("g").attr("transform", "translate(30,0)"));
*/

simulation.on("tick", function () {
  links
    .attr("x1", d => {
      return d.source.x;
    })
    .attr("y1", d => {
      return rowScale(d.source.generation);
    })
    .attr("x2", d => {
      return d.target.x;
    })
    .attr("y2", d => {
      return rowScale(d.target.generation);
    });
  nodes.attr("transform", function (d) {
    return "translate(" + d.x + "," + rowScale(d.generation) + ")";
  })
});

function dataRange(records, field) {
  var min = d3.min(records.map(record => parseInt(record[field], 10)));
  var max = d3.max(records.map(record => parseInt(record[field], 10)));
  return d3.range(min, max + 1);
};

function assignGeneration(nodes, generationNodes, generationCount) {
  const childNodes = [];
  generationNodes.forEach(function (node) {
    if (node.children) {
      // Node has children
      node.generation = generationCount + 1;
      node.children.forEach(childID => {
        if (!childNodes.find(childNode => childNode.id === childID)) {
          childNodes.push(generationNodes.find(childNode => childNode.id === childID));
        }
      })
    } else {
      if (node.partners) {
        node.partners.forEach(partnerID => {
          if (generationNodes.find(partnerNode => partnerNode.id === partnerID && partnerNode.children)) {
            // Node has partner with children
            node.generation = generationCount + 1;
          }
        })
      } else {
        // Use generation of parent + 1
        const parent = nodes.find(parentNode => parentNode.children && parentNode.children.indexOf(node.id) !== -1);
        node.generation = parent.generation + 1;
      }
    }
  });
  if (childNodes.length > 0) {
    return assignGeneration(nodes, childNodes, generationCount += 1);
  } else {
    nodes.filter(node => !node.generation).forEach(function (node) {
      node.generation = generationCount + 1;
    });
    return nodes;
  }
}

  </script>
</body>

</html>

这篇关于用d3或cytoscape渲染家谱的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆