将 d3 层次结构(d3 树)图反转到左侧以显示下游
Reverse the d3-hierarchy (d3-tree) graph to left side to show downstream as well
我有两组数据,一组是上游的,一组是下游的。上下游都有同一个主节点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 的上游和下游数据?
下面是我的codesandboxlink
https://codesandbox.io/s/d3-practice-forked-y69kkw?file=/src/index.js
提前致谢!
我已经修改了我对此 question 的回答,因此它适合您的数据结构。
此方法关键步骤:
- 请记住,对于水平布局,您可以翻转
x
和 y
...
- 计算上游和下游的树布局
- 使根节点具有相同的
x
和y
- Re-compute每个节点的
y
坐标,使得根在中心,下游分支向左,上游分支工作right-ward。
- 画出两棵树
如果你跳过第 3 步,那么你会得到这个(红色是上游,绿色是下游):
因此翻转一下,使下游树在 left-hand 侧,上游树在 right-hand 侧(根居中):
- 我们需要将上游节点的
y
坐标(即x
)减半,加上innerWidth
的一半。对于根,这放在中心,但对于后代,它按比例将它们放在右侧:
Array.from(nodesUpstream).forEach(n => n.y = (n.y * 0.5) + innerWidth / 2);
然后,对下游节点 y
坐标(实际上是 x
...)做同样的减半,但是 *-1
其中 'mirrors' 它们然后添加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>
结果是:
有 2 个限制:根被绘制了两次(我猜你可以跳过为其中一次标记 John)更重要的是,当 re-laying-out y
坐标。如果你有一个更深的上游树,你会看到这个,因为它仍然会布置在右手边,而且要多得多 'scrunched'.
编辑
要固定节点宽度(根据深度),你可以使用这个:
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>
给出:
我有两组数据,一组是上游的,一组是下游的。上下游都有同一个主节点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 的上游和下游数据?
下面是我的codesandboxlink
https://codesandbox.io/s/d3-practice-forked-y69kkw?file=/src/index.js
提前致谢!
我已经修改了我对此 question 的回答,因此它适合您的数据结构。
此方法关键步骤:
- 请记住,对于水平布局,您可以翻转
x
和y
... - 计算上游和下游的树布局
- 使根节点具有相同的
x
和y
- Re-compute每个节点的
y
坐标,使得根在中心,下游分支向左,上游分支工作right-ward。 - 画出两棵树
如果你跳过第 3 步,那么你会得到这个(红色是上游,绿色是下游):
因此翻转一下,使下游树在 left-hand 侧,上游树在 right-hand 侧(根居中):
- 我们需要将上游节点的
y
坐标(即x
)减半,加上innerWidth
的一半。对于根,这放在中心,但对于后代,它按比例将它们放在右侧:
Array.from(nodesUpstream).forEach(n => n.y = (n.y * 0.5) + innerWidth / 2);
然后,对下游节点 y
坐标(实际上是 x
...)做同样的减半,但是 *-1
其中 'mirrors' 它们然后添加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>
结果是:
有 2 个限制:根被绘制了两次(我猜你可以跳过为其中一次标记 John)更重要的是,当 re-laying-out y
坐标。如果你有一个更深的上游树,你会看到这个,因为它仍然会布置在右手边,而且要多得多 'scrunched'.
编辑
要固定节点宽度(根据深度),你可以使用这个:
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>
给出: