我有两组数据,一组用于上游,一组用于下游。上下游都有相同的John主节点。
上游数据
var upstreamData = [
{ name: "John", parent: "" },
{ name: "Ann", parent: "John" },
{ name: "Adam", parent: "John" },
{ name: "Chris", parent: "John" },
{ name: "Tina", parent: "Ann" },
{ name: "Sam", parent: "Ann" },
{ name: "Rock", parent: "Chris" },
{ name: "will", parent: "Chris" },
{ name: "Nathan", parent: "Adam" },
{ name: "Roger", parent: "Tina" },
{ name: "Dena", parent: "Tina" },
{ name: "Jim", parent: "Dena" },
{ name: "Liza", parent: "Nathan" }
];
下游数据
var downstreamData = [
{ name: "John", parent: "" },
{ name: "Kat", parent: "John" },
{ name: "Amily", parent: "John" },
{ name: "Summer", parent: "John" },
{ name: "Loki", parent: "Kat" },
{ name: "Liam", parent: "Kat" },
{ name: "Tom", parent: "Amily" }
];
我能够使用 d3 层次结构和 d3 树将上游数据表示到主节点的右侧,下图是图像
如何将下游数据表示到主节点 John 的左侧,以便我可以在同一个图中同时看到 john 的上游和下游数据?
下面是我的codesandbox的链接
https://codesandbox.io/s/d3-practice-forked-y69kkw?file=/src/index.js
提前致谢!
最佳答案
我已经调整了我的答案 question所以它适合您的数据结构。
该方法有关键步骤:
- 请记住,对于水平布局,您需要翻转
x
和y
... - 计算上游和下游的树布局
- 使根节点具有相同的
x
和y
- 重新计算每个节点的
y
坐标,使根位于中心,下游分支向左移动,上游分支向右移动。 - 画出两棵树
如果您跳过第 3 步,那么您最终会得到以下结果(其中红色为上游,绿色为下游):
因此,将其翻转,使下游树位于左侧,上游树位于右侧(并且根位于中心):
- 我们需要将上游节点的
y
坐标(即它的x
)减半,并添加一半的innerWidth
。对于根,它放在中间,但对于后代,它按比例将它们放在右侧:
Array.from(nodesUpstream).forEach(n => n.y = (n.y * 0.5) + innerWidth / 2);
然后,对下游节点 y
坐标(实际上是 x
...)进行相同的减半,但是 *-1
“镜像”它们,然后添加 innerWidth/2
回来。根仍然位于中心,但现在后代按比例位于左侧并镜像
Array.from(nodesDownstream).forEach(n => n.y = ((n.y * 0.5) * -1) + innerWidth / 2);
请参阅下面的工作片段以及您的 OP 数据:
const nodeRadius = 6;
const width = 600;
const height = 400;
const margin = { top: 24, right: 24, bottom: 24, left: 24 };
const innerWidth = width - margin.left - margin.right;
const innerHeight = height - margin.top - margin.bottom;
const svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
const rootName = "John";
const treeLayout = d3.tree().size([innerHeight, innerWidth]);
const stratified = d3.stratify()
.id(function (d) { return d.name; })
.parentId(function (d) { return d.parent; });
const linkPathGenerator = d3.linkHorizontal()
.x((d) => d.y)
.y((d) => d.x);
// create 2x trees
const nodesUpstream = treeLayout(d3.hierarchy(stratified(upstreamData)).data);
const nodesDownstream = treeLayout(d3.hierarchy(stratified(downstreamData)).data);
// align the root node x and y
const nodesUpRoot = Array.from(nodesUpstream).find(n => n.data.name == rootName);
const nodesDownRoot = Array.from(nodesDownstream).find(n => n.data.name == rootName);
nodesDownRoot.x = nodesUpRoot.x;
nodesDownRoot.y = nodesUpRoot.y;
// NOTE - COMMENT OUT THIS STEP TO SEE THE INTEMEDIARY STEP
// for horizontal layout, flip x and y...
// right hand side (upstream): halve and add width / 2 to all y's (which are for x)
Array.from(nodesUpstream).forEach(n => n.y = (n.y / 2) + innerWidth / 2);
// left hand side (downstream): halve and negate all y's (which are for x) and add width / 2
Array.from(nodesDownstream).forEach(n => n.y = ((n.y / 2) * -1) + innerWidth / 2);
// render both trees
// index allows left hand and right hand side to separately selected and styled
[nodesUpstream, nodesDownstream].forEach(function(nodes, index) {
// adds the links between the nodes
// need to select links based on index to prevent bad rendering
svg.selectAll(`links-${index}`)
.data(nodes.links())
.enter()
.append("path")
.attr("class", `link links-${index}`)
.attr("d", linkPathGenerator);
// adds each node as a group
// need to select nodes based on index to prevent bad rendering
var nodes = svg.selectAll(`.nodes-${index}`)
.data(nodes.descendants())
.enter()
.append("g")
.attr("class", `node nodes-${index}`)
.attr("transform", function(d) {
// x and y flipped here to achieve horizontal placement
return `translate(${d.y},${d.x})`;
});
// adds the circle to the node
nodes.append("circle")
.attr("r", nodeRadius);
// adds the text to the node
nodes.append("text")
.attr("dy", ".35em")
.attr("y", -20)
.style("text-anchor", "middle")
.text(function(d) { return d.data.name; });
});
body {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
margin: 0;
overflow: hidden;
}
/* upstream */
path.links-0 {
fill: none;
stroke: #ff0000;
}
/* downstream */
path.links-1 {
fill: none;
stroke: #00ff00;
}
text {
text-shadow: -1px -1px 3px white, -1px 1px 3px white, 1px -1px 3px white,
1px 1px 3px white;
pointer-events: none;
font-family: "Playfair Display", serif;
}
circle {
fill: blue;
}
<link href="https://fonts.googleapis.com/css?family=Playfair+Display" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.3.0/d3.min.js"></script>
<script>
// Upstream data
var upstreamData = [
{ name: "John", parent: "" },
{ name: "Ann", parent: "John" },
{ name: "Adam", parent: "John" },
{ name: "Chris", parent: "John" },
{ name: "Tina", parent: "Ann" },
{ name: "Sam", parent: "Ann" },
{ name: "Rock", parent: "Chris" },
{ name: "will", parent: "Chris" },
{ name: "Nathan", parent: "Adam" },
{ name: "Roger", parent: "Tina" },
{ name: "Dena", parent: "Tina" },
{ name: "Jim", parent: "Dena" },
{ name: "Liza", parent: "Nathan" }
];
// Downstream data
var downstreamData = [
{ name: "John", parent: "" },
{ name: "Kat", parent: "John" },
{ name: "Amily", parent: "John" },
{ name: "Summer", parent: "John" },
{ name: "Loki", parent: "Kat" },
{ name: "Liam", parent: "Kat" },
{ name: "Tom", parent: "Amily" }
];
</script>
有两个限制:根被绘制了两次(我猜你可以跳过其中之一的 John 标签),更重要的是,在重新布局 y 时不考虑树的深度坐标。如果你有一棵更深的上游树,你会看到这一点,因为它仍然会布置在右半边并且更加“挤压”。
编辑
要固定节点宽度(根据深度),您可以使用以下命令:
const depthFactor = 60;
Array.from(nodesUpstream).forEach(n => n.y = (n.depth * depthFactor) + innerWidth / 2);
Array.from(nodesDownstream).forEach(n => n.y = (innerWidth / 2) - (n.depth * depthFactor));
示例:
const nodeRadius = 6;
const width = 600;
const height = 400;
const margin = { top: 24, right: 24, bottom: 24, left: 24 };
const innerWidth = width - margin.left - margin.right;
const innerHeight = height - margin.top - margin.bottom;
const svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
const rootName = "John";
const treeLayout = d3.tree().size([innerHeight, innerWidth]);
const stratified = d3.stratify()
.id(function (d) { return d.name; })
.parentId(function (d) { return d.parent; });
const linkPathGenerator = d3.linkHorizontal()
.x((d) => d.y)
.y((d) => d.x);
// create 2x trees
const nodesUpstream = treeLayout(d3.hierarchy(stratified(upstreamData)).data);
const nodesDownstream = treeLayout(d3.hierarchy(stratified(downstreamData)).data);
// align the root node x and y
const nodesUpRoot = Array.from(nodesUpstream).find(n => n.data.name == rootName);
const nodesDownRoot = Array.from(nodesDownstream).find(n => n.data.name == rootName);
nodesDownRoot.x = nodesUpRoot.x;
nodesDownRoot.y = nodesUpRoot.y;
// for horizontal layout, flip x and y...
const depthFactor = 60;
Array.from(nodesUpstream).forEach(n => n.y = (n.depth * depthFactor) + innerWidth / 2);
Array.from(nodesDownstream).forEach(n => n.y = (innerWidth / 2) - (n.depth * depthFactor));
// render both trees
// index allows left hand and right hand side to separately selected and styled
[nodesUpstream, nodesDownstream].forEach(function(nodes, index) {
// adds the links between the nodes
// need to select links based on index to prevent bad rendering
svg.selectAll(`links-${index}`)
.data(nodes.links())
.enter()
.append("path")
.attr("class", `link links-${index}`)
.attr("d", linkPathGenerator);
// adds each node as a group
// need to select nodes based on index to prevent bad rendering
var nodes = svg.selectAll(`.nodes-${index}`)
.data(nodes.descendants())
.enter()
.append("g")
.attr("class", `node nodes-${index}`)
.attr("transform", function(d) {
// x and y flipped here to achieve horizontal placement
return `translate(${d.y},${d.x})`;
});
// adds the circle to the node
nodes.append("circle")
.attr("r", nodeRadius);
// adds the text to the node
nodes.append("text")
.attr("dy", ".35em")
.attr("y", -20)
.style("text-anchor", "middle")
.text(function(d) { return d.data.name; });
});
body {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
margin: 0;
overflow: hidden;
}
/* upstream */
path.links-0 {
fill: none;
stroke: #ff0000;
}
/* downstream */
path.links-1 {
fill: none;
stroke: #00ff00;
}
text {
text-shadow: -1px -1px 3px white, -1px 1px 3px white, 1px -1px 3px white,
1px 1px 3px white;
pointer-events: none;
font-family: "Playfair Display", serif;
}
circle {
fill: blue;
}
<link href="https://fonts.googleapis.com/css?family=Playfair+Display" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.3.0/d3.min.js"></script>
<script>
// Upstream data
var upstreamData = [
{ name: "John", parent: "" },
{ name: "Ann", parent: "John" },
{ name: "Adam", parent: "John" },
{ name: "Chris", parent: "John" },
{ name: "Tina", parent: "Ann" },
{ name: "Sam", parent: "Ann" },
{ name: "Rock", parent: "Chris" },
{ name: "will", parent: "Chris" },
{ name: "Nathan", parent: "Adam" },
{ name: "Roger", parent: "Tina" },
{ name: "Dena", parent: "Tina" },
{ name: "Jim", parent: "Dena" },
{ name: "Liza", parent: "Nathan" }
];
// Downstream data
var downstreamData = [
{ name: "John", parent: "" },
{ name: "Kat", parent: "John" },
{ name: "Amily", parent: "John" },
{ name: "Summer", parent: "John" },
{ name: "Loki", parent: "Kat" },
{ name: "Liam", parent: "Kat" },
{ name: "Tom", parent: "Amily" }
];
</script>
这给出:
关于javascript - 将 d3 层次结构(d3 树)图反转到左侧以也显示下游,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/71900796/