Emi*_*röm 8 javascript d3.js cytoscape.js dagre dagre-d3
我在用 Javascript 生成好看的家谱时遇到问题。
要求:
我试过的最接近这个:
使用Dagre作为布局引擎的Cytoscape JS,启用曲线样式:出租车边缘。

(随机数据图表,实线为亲子关系,虚线为配偶)
问题是配偶彼此不一致。Dagre 历来支持“等级”作为节点的参数,这意味着您可以强制某些节点处于特定高度(如果愿意,可以将其视为“一代”)。不幸的是,它不再起作用,负责的开发人员也不再从事该项目。这将很好地解决我的问题。
我尝试过但失败的其他事情:
将 dagre 降级到支持排名的旧版本?
还没有获得使用任何版本的 dagre 的等级。
和上面一样的问题,因为dagre-d3是dagre的修改版本,这意味着它不支持按代排序。
yFiles 家谱演示看起来很棒,但很商业化。对于我的目的(希望任何人建立自己的家谱),单个开发人员许可证的成本是 26.000 美元(!?!)。显然不能接受。
我的问题
是否可以像我上面描述的那样垂直对齐我的 cytoscape/dagre 图中的节点?
如果没有,我愿意尝试其他库和其他布局算法。
我正在寻找一个看起来类似于 yFiles 解决方案但使用开源工具的工作示例。
在你深入了解我的答案之前:) 你可能想看看WebCola,我在研究约束力有向图时遇到了它:
基于 JavaScript 约束的布局,使用 D3.js 和其他基于 Web 的图形库进行高质量图形可视化和探索。
它允许您指定 x 和 y 维度约束,就像我在下面的示例中对 y 维度所做的那样。我自己没有使用过它,但看起来非常适合您的要求。它与 CytoScape 一起使用,因此您可以在您已经完成的工作的基础上进行构建......
将尺寸约束应用于力有向图:
由于您没有处理严格的层次结构(例如,您不是从一个后代开始并逐步向上),一种方法是使用带有节点的D3 力有向图来表示每个家庭成员。与线性层次结构相比,这将提供更多的灵活性。
然后可以通过将节点约束到 y 轴上的固定点来实现您正在寻找的世代布局。
assignGeneration基于链接的子节点、伙伴节点和父节点计算的节点生成(或 y 坐标)希望这里有足够的内容让您决定这是否是一种可行的方法。在父母和孩子之间建立展示性的垂直/水平链接应该相当简单,但可能需要一些实验。
可能需要进行调整(取决于数据量和节点关系等)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>Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
1203 次 |
| 最近记录: |