2

I'm coding an horizontal hierarchical tree power bi custom visual using typescript and D3, using d3's treeLayout, and I need to write a link generator that can plot bezier, step AND diagonal links, at user's will.

The catch (for me) is: since the nodes are rects with certain width, the links should go from (source.x + width/2, y) to (target.x - width/2, y). I manage to do it with the diagonal and step options, using d3.line() function (which returns a path string) , but have stumbled upon the bezier option, and the linkHorizontal function (which returns a link() function). I have read the entire documentation and even the d3 code itself but so far haven't managed to use its source, target and context functions to achieve what I need.

Here's my code so far, simplified:

  • this.settings.links.style holds the user's link option "bezier", "curve" or "step"
  • this.settings.nodes.width holds the width of the node
  • this.orientation.x maps x and y functions as seen in Bostock's https://bl.ocks.org/mbostock/3184089 (the original code also considers other orientations)
const linkH = d3.linkHorizontal().x(d => this.orientation.x(d)).y(d => this.orientation.y(d));

let linkGenerator = this.settings.links.style == "bezier" ? linkH 
            :
            (this.settings.links.style == "step" ?
                d => d3.line().curve(d3.curveStep)([[this.orientation.x(d.source) + this.settings.nodes.width / 2, this.orientation.y(d.source)],
                [this.orientation.x(d.target) - this.settings.nodes.width / 2, this.orientation.y(d.target)]])
                :
                d => d3.line()([[this.orientation.x(d.source) + this.settings.nodes.width / 2, this.orientation.y(d.source)],
                [this.orientation.x(d.target) - this.settings.nodes.width, this.orientation.y(d.target)]])
            )

var links = linkGroup.selectAll("path")
              .data(this.viewModel.hierarchy.links())
              .enter()
              .append("path")
              .attr("d", linkGenerator);

2 Answers2

2

Here is a simple Bezier curve function to use instead of the D3's:

const getBezierPath = (from, to) => {
    if (Math.abs(from.x - to.x) > Math.abs(from.y - to.y)) {
    const midX = (to.x + from.x) / 2;
    return `M ${from.x},${from.y} C ${midX},${from.y} ${midX},${to.y} ${to.x},${to.y}`;
  } else {
    const midY = (to.y + from.y) / 2;
    return `M ${from.x},${from.y} C ${from.x},${midY} ${to.x},${midY} ${to.x},${to.y}`;
  }
};

See a demo in a fiddle

Michael Rovinsky
  • 6,807
  • 7
  • 15
  • 30
1

Eventually, I came up with this function that solves the goal of using both d3.line() and d3.linkHorizontal(). The key was to use a implementation of d3.DefaultLinkObject so that I could use the original source and target passed by attr("d", f(d)).

So, if it helps anybody:

class myLinkObject implements d3.DefaultLinkObject {
    public source: [number, number];
    public target: [number, number];
    constructor(s: [number, number], t: [number, number]) {
        this.source = s;
        this.target = t;
    }
}

function linkGenerator(d) {

   var deltaX = self.settings.nodes.width / 2;

   var pSource: [number, number] = [self.orientation.x(d.source) + deltaX, self.orientation.y(d.source);
   var pTarget: [number, number] = [self.orientation.x(d.target) - deltaX, self.orientation.y(d.target);

   var points = [pSource, pTarget];
   var linkObject: myLinkObject = new myLinkObject(pSource, pTarget);
   var path = "";

  if (self.settings.links.style == "step") {

      var lineGenerator = d3.line().curve(d3.curveStep);
      path = lineGenerator(points);

   } else if (self.settings.links.style == "diagonal") {

      var lineGenerator = d3.line();
      path = lineGenerator(points);

   } else {  // bezier

      var linkGen = d3.linkHorizontal().x(d => d[0]).y(d => d[1]);
      path = linkGen(linkObject);
   }
            
   return path;
}