Home > Mobile >  Using lines with arrows pointing from source to target
Using lines with arrows pointing from source to target

Time:11-15

I am trying to convert a D3 force-directed graph codebase. Source code: enter image description here

And the snippet is here:

/* eslint-disable no-undef */

var width = 600;
var height = 600;

var nodes = [
  { color: "red", size: 5 },
  { color: "orange", size: 10 },
  { color: "yellow", size: 15 },
  { color: "green", size: 20 },
  { color: "blue", size: 25 },
  { color: "purple", size: 30 }
];

var links = [
  { source: "red", target: "orange" },
  { source: "orange", target: "yellow" },
  { source: "yellow", target: "green" },
  { source: "green", target: "blue" },
  { source: "blue", target: "purple" },
  { source: "purple", target: "red" },
  { source: "green", target: "red" }
];

var svg = d3
  .select("svg")
  .attr("width", width)
  .attr("height", height);

var linkSelection = svg
  .selectAll("line")
  .data(links)
  .enter()
  .append("line")
  .attr("stroke", "black")
  .attr("stroke-width", 1);

var nodeSelection = svg
  .selectAll("circle")
  .data(nodes)
  .enter()
  .append("circle")
  .attr("r", d => d.size)
  .attr("fill", d => d.color)
  .call(
    d3
      .drag()
      .on("start", dragStart)
      .on("drag", drag)
      .on("end", dragEnd)
  );

var simulation = d3.forceSimulation(nodes);

simulation
  .force("center", d3.forceCenter(width / 2, height / 2))
  .force("nodes", d3.forceManyBody())
  .force(
    "links",
    d3
      .forceLink(links)
      .id(d => d.color)
      .distance(d => 5 * (d.source.size   d.target.size))
  )
  .on("tick", ticked);

function ticked() {
  // console.log(simulation.alpha());

  nodeSelection.attr("cx", d => d.x).attr("cy", d => d.y);

  linkSelection
    .attr("x1", d => d.source.x)
    .attr("y1", d => d.source.y)
    .attr("x2", d => d.target.x)
    .attr("y2", d => d.target.y);
}

function dragStart(d) {
  // console.log('drag start');
  simulation.alphaTarget(0.5).restart();
  d.fx = d.x;
  d.fy = d.y;
}

function drag(d) {
  // console.log('dragging');
  // simulation.alpha(0.5).restart()
  d.fx = d3.event.x;
  d.fy = d3.event.y;
}

function dragEnd(d) {
  // console.log('drag end');
  simulation.alphaTarget(0);
  d.fx = null;
  d.fy = null;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>

<svg
  version="1.1"
  baseProfile="full"
  xmlns="http://www.w3.org/2000/svg"
></svg>

But my goal is draw lines with arrows on the end, where the arrows are pointing to a target from a source. Does anyone know how to properly change the line type, and have it point the right way?

CodePudding user response:

You can create a marker representing the arrow tip in your svg, then use it in a marker-end attribute on your lines.

Here is an example: http://thenewcode.com/1068/Making-Arrows-in-SVG

CodePudding user response:

There is a reasonable solution here but it is D3 v3 and uses a path not line.

Basically, using a marker you calculate the amount to shorten your line so that it starts at the boundary of the source node (i.e. shorten by the source radius) and ends at the boundary of the target node (i.e. shorten at the end by the radius of the target node plus the dimension of the marker).

The hard-coded 12 in the snippet is the dimension of the marker - if you change e.g. markerWidth then you will probably need to play with this constant in the tick function.

So, there are 3 edits to your original code: add the marker, append the marker to the line and in tick, do the math to shorten each end and replot the line. See below:

var width = 600;
var height = 200;

var nodes = [
  { color: "red", size: 5 },
  { color: "orange", size: 10 },
  { color: "yellow", size: 15 },
  { color: "green", size: 20 },
  { color: "blue", size: 25 },
  { color: "purple", size: 30 }
];

var links = [
  { source: "red", target: "orange" },
  { source: "orange", target: "yellow" },
  { source: "yellow", target: "green" },
  { source: "green", target: "blue" },
  { source: "blue", target: "purple" },
  { source: "purple", target: "red" },
  { source: "green", target: "red" }
];

var svg = d3
  .select("svg")
  .attr("width", width)
  .attr("height", height);

// append a path marker to svg defs 
svg.append("defs").selectAll("marker")
  .data(["dominating"])
  .enter().append("marker")
  .attr("id", function(d) { return d; })
  .attr("viewBox", "0 -5 10 10")
  .attr("refX", 0)
  .attr("refY", 0)
  .attr("markerWidth", 12)
  .attr("markerHeight", 12)
  .attr("orient", "auto")
  .append("path")
  .attr("d", "M0,-5L10,0L0,5");
//

var linkSelection = svg
  .selectAll("line")
  .data(links)
  .enter()
  .append("line")
  .attr("stroke", "black")
  .attr("stroke-width", 1)
  // add marker to line
  .attr("marker-end", d => "url(#dominating)");
  //

var nodeSelection = svg
  .selectAll("circle")
  .data(nodes)
  .enter()
  .append("circle")
  .attr("r", d => d.size)
  .attr("fill", d => d.color)
  .call(
    d3
      .drag()
      .on("start", dragStart)
      .on("drag", drag)
      .on("end", dragEnd)
  );

var simulation = d3.forceSimulation(nodes);

simulation
  .force("center", d3.forceCenter(width / 2, height / 2))
  .force("nodes", d3.forceManyBody())
  .force(
    "links",
    d3
      .forceLink(links)
      .id(d => d.color)
      .distance(d => 5 * (d.source.size   d.target.size))
  )
  .on("tick", ticked);

function ticked() {
  // console.log(simulation.alpha());

  nodeSelection.attr("cx", d => d.x).attr("cy", d => d.y);

  linkSelection
    .attr("x1", d => d.source.x)
    .attr("y1", d => d.source.y)
    .attr("x2", d => d.target.x)
    .attr("y2", d => d.target.y);


  // recalculate and back off the distance
  linkSelection.each(function(d, i, n) {
    // current path length
    const pl = this.getTotalLength();
    // radius of marker head plus def constant
    const mrs = (d.source.size);
    const mrt = (d.target.size)   12;
    // get new start and end points
    const m1 = this.getPointAtLength(mrs);
    const m2 = this.getPointAtLength(pl - mrt);
    // new line start and end
    d3.select(n[i])
     .attr("x1", m1.x)
     .attr("y1", m1.y)
     .attr("x2", m2.x)
     .attr("y2", m2.y);
  });    
}

function dragStart(d) {
  // console.log('drag start');
  simulation.alphaTarget(0.5).restart();
  d.fx = d.x;
  d.fy = d.y;
}

function drag(d) {
  // console.log('dragging');
  // simulation.alpha(0.5).restart()
  d.fx = d3.event.x;
  d.fy = d3.event.y;
}

function dragEnd(d) {
  // console.log('drag end');
  simulation.alphaTarget(0);
  d.fx = null;
  d.fy = null;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<svg
  version="1.1"
  baseProfile="full"
  xmlns="http://www.w3.org/2000/svg"
></svg>

PS works on v4.13, v5.7 and v7.6.1

  • Related