将 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 的回答,因此它适合您的数据结构。

此方法关键步骤:

  1. 请记住,对于水平布局,您可以翻转 xy...
  2. 计算上游和下游的树布局
  3. 使根节点具有相同的xy
  4. Re-compute每个节点的y坐标,使得根在中心,下游分支向左,上游分支工作right-ward。
  5. 画出两棵树

如果你跳过第 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>

给出: