javascript - 将 d3 层次结构(d3 树)图反转到左侧以也显示下游

标签 javascript d3.js

我有两组数据,一组用于上游,一组用于下游。上下游都有相同的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 树将上游数据表示到主节点的右侧,下图是图像

enter image description here

如何将下游数据表示到主节点 John 的左侧,以便我可以在同一个图中同时看到 john 的上游和下游数据?

下面是我的codesandbox的链接

https://codesandbox.io/s/d3-practice-forked-y69kkw?file=/src/index.js

提前致谢!

最佳答案

我已经调整了我的答案 question所以它适合您的数据结构。

该方法有关键步骤:

  • 请记住,对于水平布局,您需要翻转 xy...
  • 计算上游和下游的树布局
  • 使根节点具有相同的xy
  • 重新计算每个节点的 y 坐标,使根位于中心,下游分支向左移动,上游分支向右移动。
  • 画出两棵树
  • 如果您跳过第 3 步,那么您最终会得到以下结果(其中红色为上游,绿色为下游):

    enter image description here

    因此,将其翻转,使下游树位于左侧,上游树位于右侧(并且根位于中心):

    • 我们需要将上游节点的 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>

    结果是: enter image description here

    有两个限制:根被绘制了两次(我猜你可以跳过其中之一的 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>

    这给出:

    enter image description here

    关于javascript - 将 d3 层次结构(d3 树)图反转到左侧以也显示下游,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/71900796/

    相关文章:

    javascript - 对数组进行排序、将值插入该数组并返回最低索引的函数

    javascript - 获取多个数组中给定索引处所有元素的平均值

    javascript - d3.js 更新时动画/过渡圆圈

    javascript - 无法在 vue.js 中读取 'Undefined' 的属性

    javascript - 使用 d3.js 中的 scale() 值无法正确绘制数据

    javascript - 创建条形作为值的总和

    javascript - d3 中的焦点+上下文图

    javascript - 如何在html/css中制作下拉登录菜单

    javascript - 在窗口调整大小时动态改变高度到最高的 div

    javascript - 使用 npm 在 macOS 上安装 webpack