typescript - Ako môžem posunúť zdrojové a cieľové body Bézierovej krivky pomocou D3 generátora odkazov?


Kódujem horizontálny hierarchický strom Power BI vlastný vizuál použitím TypeScriptu a D3, využívajúc D3 treeLayout, a potrebujem napísať generátor odkazov, ktorý dokáže vykresliť Bézierove, krokové A diagonálne odkazy podľa želania používateľa.

Háčik (pre mňa) je: keďže uzly sú obdĺžniky s určitú šírkou, odkazy by mali ísť z (source.x + width/2, y) na (target.x - width/2, y). Podarilo sa mi to urobiť s možnosťami diagonal a step, použitím funkcie d3.line() (ktorá vráti reťazec cesty), ale narazil som na možnosť bezier a funkciu linkHorizontal (ktorá vráti funkciu link()). Prečítal som si celú dokumentáciu a dokonca aj samotný kód d3, ale zatiaľ sa mi nepodarilo použiť jeho funkcie source, target a context na dosiahnutie toho, čo potrebujem.

Tu je môj doterajší kód, zjednodušený:

  • this.settings.links.style drží používateľovu možnosť odkazu „bezier“, „curve“ alebo „step“
  • this.settings.nodes.width uchováva šírku uzla
  • this.orientation.x mapuje funkcie x a y ako je vidieť v Bostockovom https://bl.ocks.org/mbostock/3184089 (pôvodný kód tiež zohľadňuje iné orientácie)
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);

Odpoveď č. 1

Tu je jednoduchá funkcia Bezierovej krivky, ktorú môžete použiť namiesto D3:

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}`;
  }
};

Pozrite si ukážku vo Fiddle

Odpoveď č. 2

Nakoniec som prišiel s touto funkciou, ktorá rieši cieľ použiť zároveň d3.line() a d3.linkHorizontal(). Kľúčom bolo použiť implementáciu d3.DefaultLinkObject, aby som mohol použiť pôvodný zdroj a cieľ predaný cez attr("d", f(d)).

Takže, ak to niekomu pomôže:

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;
}