Search code examples
d3.jstreebezierorgchart

How can I alter the bezier function used by D3's diagonal()?


I'm creating an org chart in D3 based on Bernhard Zuba's D3.js Organizational Chart. The org chart models an organization in which any given person (represented by a white square) may have a hundred or so people immediately beneath them (a very flat tree structure with a black bezier curve representing each parent-child relationship).

Here's a screencap of part of the tree: flat-tree

And here's a zoom-in on the bottom of the parent node in the above picture: enter image description here

The problem is that the links between child and parent nodes tend to all bunch up together, resulting in a very thick black line with a very gradual slope, which can be a bit of an eyesore.

The function I'm using to generate the links is as follows:

// Diagonal function
var diagonal = d3.svg.diagonal()
.source(function (d) {
    return {
        x: d.source.x + (rectW / 2),
        y: d.source.y + rectH - 10
    };
})
.target(function (d) {
    return {
        x: d.target.x + (rectW / 2),
        y: d.target.y + 10
    };
})
.projection(function (d) {
    return [d.x, d.y];
});

Here, rectW is the width of each node and rectH is the height of each node.

What I'd like to do is make some slight adjustments to the bezier function used to generate the links. Specifically, I'd like to flatten out the control points a little so that the curves at the start and end of the curve are more dramatic. If anyone can show me how to alter the function used by diagonal() to generate the bezier curve, I can figure out the rest.


Solution

  • If you look at the source code to svg.diagonal, I can't really see a direct way to adjust just the control points. You'd think you could use projection for this, but that'll transform all 4 points used to generate the path. Now, I guess we could get a little hacky with projection and do something like this:

    <!DOCTYPE html>
    <html>
    
    <head>
      <script data-require="d3@3.5.17" data-semver="3.5.17" src="https://d3js.org/d3.v3.min.js"></script>
    </head>
    
    <body>
      <script>
        var data = [{
            source: {
              x: 10,
              y: 10
            },
            target: {
              x: 200,
              y: 200
            }
          }, {
            source: {
              x: 50,
              y: 50
            },
            target: {
              x: 200,
              y: 200
            }
          }];
    
        var svg = d3.select('body')
          .append('svg')
          .attr('width', 205)
          .attr('height', 205);
    
        var diagonal = d3.svg.diagonal()
          .projection(function(d) {
            if (!this.times) this.times = 0;
            this.times++;
            console.log(this.times);
            if (this.times === 1) {
              return [d.x, d.y];
            } else if (this.times === 2) {
              return [d.x - 25, d.y]
            } else if (this.times === 3) {
              return [d.x + 25, d.y];
            } else if (this.times === 4) {
              this.times = 0;
              return [d.x, d.y];
            }
          });
    
        svg.selectAll('path')
          .data(data)
          .enter()
          .append('path')
          .attr('d', diagonal)
          .style('fill', 'none')
          .style('stroke', 'black');
      </script>
    </body>
    
    </html>


    I might be over thinking this. You'd probably just be better drawing the arc yourself:

        <!DOCTYPE html>
        <html>
    
        <head>
          <script data-require="d3@3.5.17" data-semver="3.5.17" src="https://d3js.org/d3.v3.min.js"></script>
        </head>
    
        <body>
          <script>
            var data = [{
                source: {
                  x: 10,
                  y: 10
                },
                target: {
                  x: 200,
                  y: 200
                }
              }, {
                source: {
                  x: 200,
                  y: 10
                },
                target: {
                  x: 10,
                  y: 200
                }
              }];
    
            var svg = d3.select('body')
              .append('svg')
              .attr('width', 205)
              .attr('height', 205);
    
            svg.selectAll('path')
              .data(data)
              .enter()
              .append('path')
              .attr('d', function(d){
                  var s = d.source,
                      t = d.target,
                      m = (s.y + t.y) / 2,
                      p0 = [s.x, s.y],
                      p1 = [s.x, m],
                      p2 = [t.x, m],
                      p3 = [t.x, t.y];
                  
                  // adjust constrol points
                  p1[0] -= 25;
                  p2[0] += 25;
                      
                  return "M" + p0 + "C" + p1 + " " + p2 + " " + p3;    
              })
              .style('fill', 'none')
              .style('stroke', 'black');
          </script>
        </body>
    
        </html>