在d3中将子节点接近父节点

时间:2018-05-07 04:09:36

标签: javascript html d3.js svg

我正在尝试开发一个树形图,其中有一个中心节点,它将有4个子节点。那些 子节点将有7个不同的节点,但这7个不同的节点应该显示在其父节点附近,如附图中所示。如果我试图减小值以使它们更接近,那么树的一侧(左侧或右侧)会搞砸。

这就是我所做的



line.link {
  stroke: black;
}

line.hard--link {
  stroke: black;
  stroke-width: 2px;
}

<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>JS Bin</title>
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.2.3/d3.min.js"></script>

</head>

<body>
  <svg className='spider-graph-svg'>
  </svg>
  <script>
    var data = {
      "name": "root@gmail.com",
      "children": [{
        "name": "Person Name 1",
        "children": [{
            "name": "Branch 4.1"
          }, {
            "name": "Branch 4.2"
          }, {
            "name": "Branch 4.2"
          },
          {
            "name": "Branch 4.2"
          }, {
            "name": "Branch 4.2"
          },
          {
            "name": "Branch 4.2"
          }
        ]
      }, {
        "name": "Person name 2",
        "children": [{
            "name": "Branch 4.1"
          }, {
            "name": "Branch 4.2"
          }, {
            "name": "Branch 4.2"
          },
          {
            "name": "Branch 4.2"
          }, {
            "name": "Branch 4.2"
          },
          {
            "name": "Branch 4.2"
          }
        ]
      }, {
        "name": "Person Name 3",
        "children": [{
            "name": "Branch 4.1"
          }, {
            "name": "Branch 4.2"
          }, {
            "name": "Branch 4.2"
          },
          {
            "name": "Branch 4.2"
          }, {
            "name": "Branch 4.2"
          },
          {
            "name": "Branch 4.2"
          }
        ]
      }, {
        "name": "Person Name 4",
        "children": [{
            "name": "Branch 4.1"
          }, {
            "name": "Branch 4.2"
          }, {
            "name": "Branch 4.2"
          },
          {
            "name": "Branch 4.2"
          }, {
            "name": "Branch 4.2"
          },
          {
            "name": "Branch 4.2"
          }
        ]
      }]
    };


    const LAST_CHILDREN_WIDTH = 13;
    let flagForChildren = false;
    let groups = [];
    data.children.forEach(d => {
      let a = [];
      if (d.children.length > 0) {
        flagForChildren = true;
      }
      for (let i = 0; i < d.children.length; i += 2) {
        let b = d.children.slice(i, i + 2);
        if (b[0] && b[1]) {
          a.push(Object.assign(b[0], {
            children: [b[1]]
          }));
        } else {
          let child = b[0];
          if (i >= 6) {
            child = Object.assign(child, {
              children: [{
                name: "..."
              }]
            });
          }
          a.push(child);
        }
      }
      d.children = a;
      groups.push(d);
    });

    data.children = groups;
    let split_index = Math.round(data.children.length / 2);
    let rectangleHeight = 45;
    let leftData = {
      name: data.name,
      children: JSON.parse(JSON.stringify(data.children.slice(0, split_index)))
    };
    let leftDataArray = [];
    leftDataArray.push(leftData);

    // Right data
    let rightData = {
      name: data.name,
      children: JSON.parse(JSON.stringify(data.children.slice(split_index)))
    };
    // Create d3 hierarchies
    let right = d3.hierarchy(rightData);
    let left = d3.hierarchy(leftData);
    // Render both trees
    drawTree(right, "right");
    drawTree(left, "left");

    // draw single tree
    function drawTree(root, pos) {
      let SWITCH_CONST = 1;
      if (pos === "left") {
        SWITCH_CONST = -1;
      }
      const margin = {
          top: 20,
          right: 120,
          bottom: 20,
          left: 120
        },
        width = window.innerWidth - margin.left - margin.right,
        height = 500 - margin.top - margin.bottom;

      let svg = d3
        .select("svg")
        .attr("height", height + margin.top + margin.bottom)
        .attr("width", width + margin.right + margin.left)
        .attr('view-box', '0 0 ' + (width + margin.right) + ' ' + (height + margin.top + margin.bottom))
        .style("margin-top", "20px")
        .style("margin-left", "88px");

      const div = d3.select("body").append("div")
        .attr("class", "tooltip")
        .style("opacity", 0);

      // Shift the entire tree by half it's width
      let g = svg.append("g").attr("transform", "translate(" + width / 2 + ",0)");

      let deductWidthValue = flagForChildren ? 0 : width * 0.33;
      // Create new default tree layout
      let tree = d3
        .tree()
        // Set the size
        // Remember the tree is rotated
        // so the height is used as the width
        // and the width as the height
        .size([height - 50, SWITCH_CONST * (width - deductWidthValue) / 2])
        .separation((a, b) => a.parent === b.parent ? 4 : 4.25);

      tree(root);

      let nodes = root.descendants();
      let links = root.links();
      // Set both root nodes to be dead center vertically
      nodes[0].x = height / 2;

      // Create links
      let link = g
        .selectAll(".link")
        .data(links)
        .enter();

      link
        .append("line")
        .attr("class", function(d) {
          if (d.target.depth === 2) {
            return 'link'
          } else {
            return 'hard--link'
          }
        })
        .attr("x1", function(d) {
          if (
            d.target.depth === 3
          ) {
            return 0;
          }
          return d.source.y + 100 / 2; //d.source.y + 100/2
        })
        .attr("x2", function(d) {
          if (
            d.target.depth === 3
          ) {
            return 0;
          } else if (d.target.depth === 2) {
            return d.target.y;
          }
          return d.target.y + 100 / 2; //d.target.y + 100/2;
        })
        .attr("y1", function(d) {
          if (
            d.target.depth === 3
          ) {
            return 0;
          }
          return d.source.x + 50 / 2;
        })
        .attr("y2", function(d) {
          if (
            d.target.depth === 3
          ) {
            return 0;
          } else if (d.target.depth === 2) {
            return d.target.x + LAST_CHILDREN_WIDTH / 2;
          }
          return d.target.x + 50 / 2;
        });

      //Rectangle width

      let node = g
        .selectAll(".node")
        .data(nodes)
        .enter()
        .append("g")
        .on("mouseover", function(d) {
          const dynamicLength = (d.data.topic_name && d.data.topic_name.length) ||
            (d.data.name && d.data.name.length);
          const rectWidth = dynamicLength <= 3 ? '60px' : `${dynamicLength * 8}px`;
          div.transition()
            .duration(200)
            .style("opacity", 1);
          div.html(d.data.topic_name || d.data.name)
            .style("left", (d3.event.pageX) + "px")
            .style("width", rectWidth)
            .style("text-anchor", "middle")
            .style("vertical-align", "baseline")
            .style("top", (d3.event.pageY - 28) + "px");
        })
        .on("mouseout", d => {
          div.transition()
            .duration(500)
            .style("opacity", 0);
        })
        .attr("class", function(d) {
          return "node" + (d.children ? " node--internal" : " node--leaf");
        })
        .attr("transform", function(d) {
          if (d.parent && d.parent.parent) { // this is the leaf node
            if (d.parent.parent.parent) {
              return (
                "translate(" +
                d.parent.y +
                "," +
                (d.x + LAST_CHILDREN_WIDTH + 15) +
                ")"
              );
            }
            return "translate(" + d.y + "," + d.x + ")";
          }
          // Select the node with height 2
          if (d.height === 2) {
            //Lets line this up with its 2nd child (index = 1)
            //If y of this child is <0, it means the parent and the child
            //both are on the left
            //side (with margin of 20 between parent and child)
            if (d.children[1]['y'] < 0) {
              return "translate(" + (d.children[1]['y'] + LAST_CHILDREN_WIDTH + 20) + "," + d.children[1]['x'] + ")"
              // Else both parent and child are on the right.
              //Now we also need to take into consideration the width
              //of the rectangle (with margin of 20 between parent and child)
            } else {
              return "translate(" + (d.children[1]['y'] - rectangleWidth(d) - 20) + "," + (d['x']) + ")"
            }
          } else {
            //This is the root of the tree.
            //Subtract d.y by half of rectangleWidth because we need it to be in the center
            //Same for d.x
            return "translate(" + (d.y - (rectangleWidth(d) / 2)) + "," + (d.x - (rectangleHeight / 2)) + ")";
          }
        });

      // topic rect
      node
        .append("rect")
        .attr("height", (d, i) => d.parent && d.parent.parent ? 15 : rectangleHeight)
        .attr("width", (d, i) => d.parent && d.parent.parent ? 15 : rectangleWidth(d))
        .attr("rx", (d, i) => d.parent && d.parent.parent ? 5 : 5)
        .attr("ry", (d, i) => d.parent && d.parent.parent ? 5 : 5)

      // topic edges
      node.append('line')
        .attr('x1', d => {
          if (d.depth === 2) {
            return 10
          }
        })
        .attr('x2', d => {
          if (d.depth === 2) {
            return 10
          }
        })
        .attr('y1', d => {
          if (d.depth === 2) {
            if (d.children) {
              return 0;
            }
            return 40;
          }
        })
        .attr('y2', d => {
          if (d.depth === 2) {
            return 40
          }
        })
        .attr('class', 'hard--link')

      // topic names
      node
        .append("text")
        .attr("dy", function(d, i) {
          return d.parent && d.parent.parent ? 10 : rectangleHeight / 2;
        })
        .attr("dx", function(d, i) {
          if (!(d.parent && d.parent.parent)) {
            return 12;
          } else {
            return 20;
          }
        })
        .style("fill", function(d, i) {
          return d.parent && d.parent.parent ? "Black" : "White";
        })
        .text(function(d) {
          let name = d.data.topic_name || d.data.name;
          return name.length > 12 ? `${name.substring(0, 12)}...` : name;
        })
        .style("text-anchor", function(d) {
          if (d.parent && d.parent.parent) {
            return pos === "left" && "end"
          }
        })
        .style("font-size", "12")
        .attr("transform", function(d) {
          if (d.parent && d.parent.parent) {
            return pos === "left" ? "translate(-30,0)" : "translate(5,0)"
          }
        })
    }

    function rectangleWidth(d) {
      const MIN_WIDTH = 50;
      const MAX_WIDTH = 100;
      let dynamicLength = 6;
      if (d.data.topic_name) {
        dynamicLength = d.data.topic_name.length;
      } else if (d.data.name) {
        dynamicLength = d.data.name.length;
      }
      dynamicLength = dynamicLength < 3 ? MIN_WIDTH : MAX_WIDTH;
      return dynamicLength;
    }
  </script>
</body>

</html>
&#13;
&#13;
&#13;

预期的设计

最后一个节点的预期设计

enter image description here

1 个答案:

答案 0 :(得分:1)

节点的xyd3计算,但展示位置看起来不正确可能是因为heightwidth rects的{​​{1}}没有被考虑在内。

因此,我根据d3的计算rectsx翻译y部分中的代码进行了一些更改,如下所示:

.attr("transform", function (d) {
    if (d.parent && d.parent.parent) { // this is the leaf node
        if (d.parent.parent.parent) {
            return (
                "translate(" +
                d.parent.y +
                "," +
                (d.x + LAST_CHILDREN_WIDTH + 15) +
                ")"
            );
        }
        return "translate(" + d.y + "," + d.x + ")";
    }
        // Select the node with height 2
    if (d.height == 2) {
            //Lets line this up with its 2nd child (index = 1)
            //If y of this child is <0, it means the parent and the child 
            //both are on the left 
            //side (with margin of 20 between parent and child)
        if (d.children[1]['y'] < 0) {
            return "translate(" + (d.children[1]['y'] + LAST_CHILDREN_WIDTH + 20) + "," + d.children[1]['x'] + ")"
            // Else both parent and child are on the right. 
            //Now we also need to take into consideration the width 
            //of the rectangle (with margin of 20 between parent and child)
        } else {
            return "translate(" + (d.children[1]['y'] - rectangleWidth(d) - 20) + "," + (d['x']) + ")"
        }
    } else {
            //This is the root of the tree.
            //Subtract d.y by half of rectangleWidth because we need it to be in the center
            //Same for d.x
        return "translate(" + (d.y - (rectangleWidth(d) / 2)) + "," + (d.x - (rectangleHeight / 2)) + ")";
    }
});

这是这些变化的小提琴。

body {
  background: black
}

rect {
  fill: darkgreen
}

line {
  stroke: lightgreen;
  stroke-width: 1
}

text {
  font-family: 'Calibri';
}

.tooltip {
  color: white
}
<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>JS Bin</title>
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.2.3/d3.min.js"></script>

</head>

<body>
  <svg className='spider-graph-svg'>
  </svg>
  <script>
    var data = {
      "name": "root@gmail.com",
      "children": [{
        "name": "Person Name 1",
        "children": [{
            "name": "Branch 4.1"
          }, {
            "name": "Branch 4.2"
          }, {
            "name": "Branch 4.2"
          },
          {
            "name": "Branch 4.2"
          }, {
            "name": "Branch 4.2"
          },
          {
            "name": "Branch 4.2"
          }
        ]
      }, {
        "name": "Person name 2",
        "children": [{
            "name": "Branch 4.1"
          }, {
            "name": "Branch 4.2"
          }, {
            "name": "Branch 4.2"
          },
          {
            "name": "Branch 4.2"
          }, {
            "name": "Branch 4.2"
          },
          {
            "name": "Branch 4.2"
          }
        ]
      }, {
        "name": "Person Name 3",
        "children": [{
            "name": "Branch 4.1"
          }, {
            "name": "Branch 4.2"
          }, {
            "name": "Branch 4.2"
          },
          {
            "name": "Branch 4.2"
          }, {
            "name": "Branch 4.2"
          },
          {
            "name": "Branch 4.2"
          }
        ]
      }, {
        "name": "Person Name 4",
        "children": [{
            "name": "Branch 4.1"
          }, {
            "name": "Branch 4.2"
          }, {
            "name": "Branch 4.2"
          },
          {
            "name": "Branch 4.2"
          }, {
            "name": "Branch 4.2"
          },
          {
            "name": "Branch 4.2"
          }
        ]
      }]
    };


    const LAST_CHILDREN_WIDTH = 13;
    let flagForChildren = false;
    let groups = [];
    data.children.forEach(d => {
      let a = [];
      if (d.children.length > 0) {
        flagForChildren = true;
      }
      for (let i = 0; i < d.children.length; i += 2) {
        let b = d.children.slice(i, i + 2);
        if (b[0] && b[1]) {
          a.push(Object.assign(b[0], {
            children: [b[1]]
          }));
        } else {
          let child = b[0];
          if (i >= 6) {
            child = Object.assign(child, {
              children: [{
                name: "..."
              }]
            });
          }
          a.push(child);
        }
      }
      d.children = a;
      groups.push(d);
    });

    data.children = groups;
    let split_index = Math.round(data.children.length / 2);
    let rectangleHeight = 45;
    let leftData = {
      name: data.name,
      children: JSON.parse(JSON.stringify(data.children.slice(0, split_index)))
    };
    let leftDataArray = [];
    leftDataArray.push(leftData);

    // Right data
    let rightData = {
      name: data.name,
      children: JSON.parse(JSON.stringify(data.children.slice(split_index)))
    };
    // Create d3 hierarchies
    let right = d3.hierarchy(rightData);
    let left = d3.hierarchy(leftData);
    console.log(right.descendants())
    // Render both trees
    drawTree(right, "right");
    drawTree(left, "left");

    // draw single tree
    function drawTree(root, pos) {
      let SWITCH_CONST = 1;
      if (pos === "left") {
        SWITCH_CONST = -1;
      }
      const margin = {
          top: 10,
          right: 10,
          bottom: 10,
          left: 10
        },
        width = window.innerWidth - margin.left - margin.right,
        height = window.innerHeight - margin.top - margin.bottom;

      let svg = d3
        .select("svg")
        .attr("height", height + margin.top + margin.bottom)
        .attr("width", width + margin.right + margin.left)
        .attr('view-box', '0 0 ' + (width + margin.right) + ' ' + (height + margin.top + margin.bottom))
      /* .style("margin-top", "20px")
      .style("margin-left", "88px"); */

      const div = d3.select("body").append("div")
        .attr("class", "tooltip")
        .style("opacity", 0);

      // Shift the entire tree by half it's width
      let g = svg.append("g").attr("transform", "translate(" + width / 2 + ",0)");

      let deductWidthValue = flagForChildren ? 0 : width * 0.33;
      // Create new default tree layout
      let tree = d3
        .tree()
        // Set the size
        // Remember the tree is rotated
        // so the height is used as the width
        // and the width as the height
        .size([height - 50, SWITCH_CONST * (width - deductWidthValue) / 2])
        .separation((a, b) => a.parent === b.parent ? 4 : 4.25);

      tree(root);

      let nodes = root.descendants();
      let links = root.links();
      // Set both root nodes to be dead center vertically
      nodes[0].x = height / 2;

      // Create links
      let link = g
        .selectAll(".link")
        .data(links)
        .enter();

      link
        .append("line")
        .attr("class", function(d) {
          if (d.target.depth === 2) {
            return 'link'
          } else {
            return 'hard--link'
          }
        })
        .attr("x1", function(d) {
          if (d.target.depth === 3) {
            return 0;
          } else if (d.target.depth === 2) {
            if (d.source.y < 0) {
              return (d.source.y + 100 / 2) - 100;
            } else {
              return (d.source.y + 100 / 2)
            }
          }
          return 0; //d.source.y + 100/2
        })
        .attr("x2", function(d) {
          if (d.target.depth === 3) {
            return 0;
          } else if (d.target.depth === 2) {
            return d.target.y + 10;
          } else if (d.target.depth === 1) {
            if (d.target.y < 0) {
              return d.target.y - 100 / 2
            } else {
              return d.target.y + 100 / 2;
            }
          }
          return d.target.y + 100 / 2; //d.target.y + 100/2;
        })
        .attr("y1", function(d) {
          if (d.target.depth === 3) {
            return 0;
          } else if (d.target.depth === 1) {
            return (d.source.x + 50 / 2) - 20;
          } else {
            return d.source.x + 50 / 2;
          }
        })
        .attr("y2", function(d) {
          if (d.target.depth === 3) {
            return 0;
          } else if (d.target.depth === 2) {
            return d.target.x + LAST_CHILDREN_WIDTH / 2;
          }
          return d.target.x + 50 / 2;
        });

      //Rectangle width

      let node = g
        .selectAll(".node")
        .data(nodes)
        .enter()
        .append("g")
        .on("mouseover", function(d) {
          const dynamicLength = (d.data.topic_name && d.data.topic_name.length) ||
            (d.data.name && d.data.name.length);
          const rectWidth = dynamicLength <= 3 ? '60px' : `${dynamicLength * 8}px`;
          div.transition()
            .duration(200)
            .style("opacity", 1);
          div.html(d.data.topic_name || d.data.name)
            .style("left", (d3.event.pageX) + "px")
            .style("width", rectWidth)
            .style("text-anchor", "middle")
            .style("vertical-align", "baseline")
            .style("top", (d3.event.pageY - 28) + "px");
        })
        .on("mouseout", d => {
          div.transition()
            .duration(500)
            .style("opacity", 0);
        })
        .attr("class", function(d) {
          return "node" + (d.children ? " node--internal" : " node--leaf");
        })
        .attr("transform", function(d) {
          if (d.parent && d.parent.parent) { // this is the leaf node
            if (d.parent.parent.parent) {
              return (
                "translate(" +
                d.parent.y +
                "," +
                (d.x + LAST_CHILDREN_WIDTH + 15) +
                ")"
              );
            }
            return "translate(" + d.y + "," + d.x + ")";
          }
          // Select the node with height 2
          if (d.height == 2) {
            //Lets line this up with its 2nd child (index = 1)
            //If y of this child is <0, it means the parent and the child 
            //both are on the left 
            //side (with margin of 20 between parent and child)
            if (d.children[1]['y'] < 0) {
              return "translate(" + (d.children[1]['y'] + LAST_CHILDREN_WIDTH + 20) + "," + d.children[1]['x'] + ")"
              // Else both parent and child are on the right. 
              //Now we also need to take into consideration the width 
              //of the rectangle (with margin of 20 between parent and child)
            } else {
              return "translate(" + (d.children[1]['y'] - rectangleWidth(d) - 20) + "," + (d['x']) + ")"
            }
          } else {
            //This is the root of the tree.
            //Subtract d.y by half of rectangleWidth because we need it to be in the center
            //Same for d.x
            return "translate(" + (d.y - (rectangleWidth(d) / 2)) + "," + (d.x - (rectangleHeight / 2)) + ")";
          }
        })
        .on('click', function(d) {
          console.log(d)
        });

      // topic rect
      node
        .append("rect")
        .attr("height", (d, i) => d.parent && d.parent.parent ? 15 : rectangleHeight)
        .attr("width", (d, i) => d.parent && d.parent.parent ? 15 : rectangleWidth(d))
        .attr("rx", (d, i) => d.parent && d.parent.parent ? 5 : 5)
        .attr("ry", (d, i) => d.parent && d.parent.parent ? 5 : 5)

      // topic edges
      node.append('line')
        .attr('x1', d => {
          if (d.depth === 2) {
            return 10
          }
        })
        .attr('x2', d => {
          if (d.depth === 2) {
            return 10
          }
        })
        .attr('y1', d => {
          if (d.depth === 2) {
            if (d.children) {
              return 0;
            }
            return 40;
          }
        })
        .attr('y2', d => {
          if (d.depth === 2) {
            return 40
          }
        })
        .attr('class', 'hard--link')

      // topic names
      node
        .append("text")
        .attr("dy", function(d, i) {
          return d.parent && d.parent.parent ? 10 : rectangleHeight / 2;
        })
        .attr("dx", function(d, i) {
          if (!(d.parent && d.parent.parent)) {
            return 12;
          } else {
            return 20;
          }
        })
        .style("fill", function(d, i) {
          return d.parent && d.parent.parent ? "White" : "White";
        })
        .text(function(d) {
          let name = d.data.topic_name || d.data.name;
          return name.length > 12 ? `${name.substring(0, 12)}...` : name;
        })
        .style("text-anchor", function(d) {
          if (d.parent && d.parent.parent) {
            return pos === "left" && "end"
          }
        })
        .style("font-size", "12")
        .attr("transform", function(d) {
          if (d.parent && d.parent.parent) {
            return pos === "left" ? "translate(-30,0)" : "translate(5,0)"
          }
        })
    }

    function rectangleWidth(d) {
      const MIN_WIDTH = 50;
      const MAX_WIDTH = 100;
      let dynamicLength = 6;
      if (d.data.topic_name) {
        dynamicLength = d.data.topic_name.length;
      } else if (d.data.name) {
        dynamicLength = d.data.name.length;
      }
      dynamicLength = dynamicLength < 3 ? MIN_WIDTH : MAX_WIDTH;
      return dynamicLength;
    }
  </script>
</body>

</html>