用d3或cytoscape渲染家谱。

问题描述 投票:1回答:1

我在用Javascript生成一个好看的家谱时遇到了问题。

要求。

  • 每个孩子都应该连接到树上的两个父母,而不是像某些图表中的一个。
  • 我喜欢夫妻双方在树上相邻(同一垂直位置)。
  • 我想把节点按辈分纵向组织,这样就可以一目了然地看到同一十年出生的人。
  • 一个人可以有多个配偶,随着时间的推移,他们每个人都有孩子。
  • 父母和子女可以在树上自由添加,所以不只是 "从一个人向上追溯世系"

我试过的最接近这个的。

  1. Cytoscape JSDagre 作为布局引擎,以及 曲线式:出租车 启用边缘。 Family tree

    (图中数据为随机数据,实线为父子关系,虚线为配偶关系) 实线为父子关系,虚线为配偶关系。)

    问题是配偶之间不一致。Dagre历来支持将 "等级 "作为节点的参数,这意味着你可以强制一些节点处于特定的高度(如果你愿意的话,可以将其视为 "代")。不幸的是,它 不灵了负责任的开发商不再从事该项目了。. 这将很好地解决我的问题。

其他的事情我都试过了,但是失败了。

  1. 把dagre降级到支持rank的旧版本? Downgrading dagre to an older version that has support for rank?

    在任何版本的dagre中都没有得到rank的支持。

  2. D3dagre-d3

    和上面的问题一样,因为dagre-d3是dagre的修改版,也就是说它不支持按代排序。

  3. yFiles家族树 演示看起来很棒,但却是商业化的。对于我的目的(希望任何人都能建立自己的家谱)的成本是26.000美元(!!!)一个单一的开发者许可证。显然是不能接受的。

    yFiles family tree

我的问题是

是否可以像我上面描述的那样,让我的cytoscapedagre图中的节点垂直对齐?

如果不能,我愿意尝试其他库和其他布局算法。

我正在寻找一个类似于yFiles解决方案的工作实例,但使用的是开源工具。

javascript d3.js cytoscape.js dagre-d3 dagre
1个回答
6
投票

在你深入了解我的答案之前 :) 你可能想看看 网易考拉,这是我在研究约束力定向图时遇到的。

基于JavaScript约束的布局,使用D3.js和其他基于网络的图形库进行高质量的图形可视化和探索。

它允许你 指定x和y维度约束 就像我在下面的例子中对y维度所做的那样。我自己没有用过它,但看起来很适合你的要求。而且它可以和 CytoScape 一起使用,所以你也许可以在你已经完成的基础上再进一步......。

将维度约束应用于力导向图。

因为你不是在处理一个严格的层次结构 (例如,你不是从一个后裔开始,然后一路向上),一种方法是使用 D3力导向图 用一个节点来代表每个家庭成员。与线性层次结构相比,这将提供更多的灵活性。

然后,通过将节点限制在y轴上的固定点,就可以实现你所寻找的世代布局。

下面是一个概念验证:

  • 三代家庭成员
  • 多个配偶由Alice和Bob Bob和Carol代表。
  • 大卫是爱丽丝和鲍勃的孩子
  • 詹姆斯是鲍勃和卡罗尔的孩子
  • 节点生成(或y坐标)计算方法为 assignGeneration 基于关联的子节点、伙伴节点和父节点的
  • 节点X坐标由d3处理,我认为这将比试图手动为每个节点分配一个X轴上的位置更稳健。
  • 基本造型。
    • 伙伴链接为珊瑚色
    • 儿童链接为浅蓝色
    • 兄弟姐妹之间的联系为浅绿色

希望这里有足够的内容让你决定这是否是一个可行的方法。设置父母和子女之间的纵向水平链接应该是相当直接的,但可能需要一点实验。

调整(取决于数据量和节点关系等)可能需要应用于 simulation - 同样,需要进行一些实验以产生最佳布局。更多关于不同力量的信息 此处.

<!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>
© www.soinside.com 2019 - 2024. All rights reserved.