0

I'm using d3-dag to build a graph.

I have all the graph nodes in place and placed the edges lines in the right place but the only thing I'm missing is the arrows pointer to point to the right direction. Not all the arrow pointers point to the right place and are visible.

The reason I'm using the edges line curve as curveStepBefore is because I need to place another element on top of the line so I need the curve to be curveStepBefore.

I created a simple fiddle to demonstrate:

The edges drawing is done on function drawEdges, line 181

Fiddle

const data = [
    {
        "id": "0",
            "name": "node1",            
            "parentIds": []
    },
    {
        "id": "1",
            "name": "node2",
        "parentIds": ["0"]
    },
    {
        "id": "2",
            "name": "node3",
        "parentIds": ["1"]
    },
    {
        "id": "3",
            "name": "node4",
        "parentIds": ["1"]
    },
    {
        "id": "4",
            "name": "node5",
        "parentIds": ["1", "2", "3", "8"]
    },
    {
        "id": "5",
            "name": "node6",
        "parentIds": ["4"]
    },
    {
        "id": "6",
            "name": "node7",
        "parentIds": ["5"]
    },
    {
        "id": "7",
            "name": "node8",
        "parentIds": []
    },
    {
        "id": "8",
            "name": "node9",
        "parentIds": ["7"]
    }
];

async function createGraph() {
   
  const dag = d3.dagStratify()(data);
  const layout = d3
        .sugiyama() // base layout
        .decross(d3.decrossOpt()) // minimize number of crossings
        .nodeSize((node) => [200, 200]); // set node size instead of constraining to fit

  layout(dag);
  // This code only handles rendering
  const svgSelection = d3.select("#TargetContainer");
  svgSelection.append("defs"); // For gradients

  addDefs(svgSelection);

  // Initialize color map
  const steps = dag.size();
  const interp = d3.interpolateRainbow;
  const colorMap = {};
  for (const [i, node] of [...dag].entries()) {
    colorMap[node.data.id] = interp(i / steps);
  }

  drawEdges(svgSelection, dag);

  // Select nodes
  const nodes = svgSelection
    .append("g")
    .selectAll("g")
    .data(dag.descendants())
    .enter()
    .append("g")
    .attr("transform", ({ x, y }) => `translate(${x - 70}, ${y})`);

    nodes
    .append("rect")
    .attr("class", "node")
    .attr("filter", "url(#nodeShadow)")
    .attr("rx", "17")
    .attr("fill", "#FFFFFF")
    .attr("width", "150")
    .attr("height", 30)
    .each(function (p, j) {
        d3.select(this.parentElement)
            .append("circle")
            .attr("cx", 15)
            .attr("cy", 15)
            .attr("r", 15)
            .attr("fill", "#FFFFFF")
            .attr("filter", "url(#circleShadow)")
            .attr("stroke", "lightgray")
            .attr("stroke-width", "0.1")
            .attr("width", 15)
            .attr("height", 15);

        d3.select(this.parentElement)
            .append("text")
            .attr("x", 40)
            .attr("y", 19)
            .attr("class", "graph-element-text node-text-color")
            //.text(function () { return graphNode.displayName });
            .text(p.data.name);

            d3.select(this.parentElement).append("image")
            .attr("xlink:href", p.data.icon)
            .attr("x", 0)
            .attr("y", 1)
            .attr("transform", "translate(7, 6.5)")
            .attr("width", 15)
            .attr("height", 15);
    });
}

function addDefs(svg) {
    svg
    .select("defs")
    .append("svg:marker")
    .attr("id", "arrowStart")
    .attr("refX", -5)
    .attr("refY", 9)
    .attr("markerWidth", 22)
    .attr("markerHeight", 22)
    .attr("orient", "auto")
    .append("circle")
    .attr("cx", "4")
    .attr("cy", "9")
    .attr("r", "3")
    .attr("orient", "auto")
    .style("stroke", "#3278E0")
    .style("fill", "none");

    svg
    .append("defs")
    .append("svg:marker")
    .attr("id", "arrowEnd")
    .attr("refX", 6)
    .attr("refY", 6)
    .attr("markerWidth", 12)
    .attr("markerHeight", 12)
    .attr("orient", "auto")
    .append("path")
    .attr("d", "M-5,-3 L7,6 L-8,20")
    .style("stroke", "#3278E0")
    .style("fill", "none");

    svg
    .select("defs")
    .append("filter")
    .attr("width", "115%")
    .attr("height", "115%")
    .attr("id", "nodeShadow")
    .append("feDropShadow")
    .attr("dx", 0)
    .attr("dy", 1)
    .attr("stdDeviation", 1)
    .attr("flood-opacity", 1)
    .attr("flood-color", "lightgray");

    svg
    .select("defs")
    .append("filter")
    .attr("id", "circleShadow")
    .attr("width", "120%")
    .attr("height", "120%")
    .append("feDropShadow")
    .attr("dx", -1)
    .attr("dy", 1)
    .attr("stdDeviation", 3)
    .attr("flood-opacity", 1)
    .attr("flood-color", "lightgray");
}

function drawEdges(svgSelection, dag) {
    // How to draw edges
  const curve = d3.curveStepBefore;
  const line = d3
    .line()
    .curve(curve)
    .x((d) => d.x)
    .y((d) => d.y);

    svgSelection
        .append("g")
        .selectAll("path")
        .data(dag.links())
        .enter()
        .append("path")
        .attr("d", ({ points }) => line(points))
        .attr("fill", "none")
        .attr("marker-end", "url(#arrowEnd)")
        .attr("stroke-width", 3)
        .attr("stroke", "#4F97FF");
}

createGraph();
Erik
  • 6,470
  • 5
  • 36
  • 37
Ace
  • 831
  • 2
  • 8
  • 28

1 Answers1

0

If you must use curveStepBefore. Then how to set up the right marker should be relatively straightforward with a little math. d3-dag will give you a set of points for each link, which will end with to final points [..., { x: x1, y: y1}, {x: x2, y: y2 }]. It's guaranteed that x2, y2 is the center of the target node, so we just need to figure out where the marker would be. With curveStepBefore we know the marker will have the same y position, so we just need to figure out the x. Assuming your node is 2 * w wide the marker position should be x1 > x2 ? Math.min(x2 + w, x1) : Math.min(x2 - w, x1).

However, I don't think you need to use curveStepBefore if you want to add edge labels. You should be able to find an appropriate point using the points on the line, in a worst case finding a midpoint and either adding it to get spline interpolation, or just assuming it will be close enough.

Erik
  • 6,470
  • 5
  • 36
  • 37