Как создать пунктирную линию со стрелками на каждом да sh в d3? - PullRequest
0 голосов
/ 21 февраля 2020

Как сделать линию из стрелок, как пунктирную линию, но примерно как ">>>>" для направления потока. Я пытаюсь соединить узлы через линии, но мои узлы должны иметь изменяемые размеры прямоугольников, и это скрывает стрелки, поэтому я ищу такую ​​линию стрелки (ed). Указание стрелки в середине также может решить проблему, но attr ("marker-middle", "url (#arrowhead)") не работает в моем коде.

Вот соответствующий код

svg
    .append("defs")
    .append("marker")
    .attrs({
      id: "arrowhead",
      viewBox: "-0 -5 10 10",
      refX: 13,
      refY: 0,
      orient: "auto",
      markerWidth: 3,
      markerHeight: 3
    })
    .append("svg:path")
    .attr("d", "M 0,-5 L 10 ,0 L 0,5")
    .attr("fill", "black")
    .call(
      d3.zoom().on("zoom", function() {
        console.log(d3.event.transform);
        svg.attr("transform", d3.event.transform);
      })
    );


var link = svg
    .append("g")
    .selectAll("line_class.line")
    .data(graph.links)
    .enter()
    .append("line")
    .attr("stroke-width", function(d) {
      return Math.max(
        ((d.thickness - min_thickness) /
          (max_thickness - min_thickness + 0.01)) *
          5,
        3
      );
    })
    .style("stroke", "pink")
    .text("text", function(d) {
      return d.linkname;
    })
    .on("mouseover", function(d) {
      div
        .transition()
        .duration(200)
        .style("opacity", 0.9);
      div
        .html("Name: " + d.linkname)
        .style("left", d3.event.pageX + "px")
        .style("top", d3.event.pageY - 28 + "px")
        .style("width", "auto")
        .style("height", "auto");
    })
    .on("mouseout", function(d) {
      div
        .transition()
        .duration(500)
        .style("opacity", 0);
    });


link.call(updateState1);


function updateState1() {
link 

      .each(function(d) {
//d3.select(this).attr("marker-mid", "url(#arrowhead)");
d3.select(this).attr("marker-end", "url(#arrowhead)");

      });
  }

1 Ответ

0 голосов
/ 21 февраля 2020

var config = [];

var graph = {
  nodes: [
    { name: "A" },
    { name: "B" },
    { name: "C" },
    { name: "D" },
    { name: "Dummy1" },
    { name: "Dummy2" },
    { name: "Dummy3" },
    { name: "Dummy4" }
  ],

  links: [
    { source: "A", target: "B", linkname: "A0" },
    { source: "A", target: "C", linkname: "A0" },
    { source: "A", target: "D", linkname: "A1" },
    { source: "Dummy1", target: "A", linkname: "input" },
    { source: "B", target: "Dummy2", linkname: "B" },
    { source: "C", target: "Dummy3", linkname: "C" },
    { source: "D", target: "Dummy4", linkname: "D" }
  ]
};

var optArray = [];
labelAnchors = [];
labelAnchorLinks = [];

highlightNode_button = d3.select("#highlightNode");
highlightNode_button.on("click", highlightNode);

shrinkNode_button = d3.select("#shrinkNode");
shrinkNode_button.on("click", minimizeNode);

for (var i = 0; i < graph.nodes.length - 1; i++) {
  optArray.push(graph.nodes[i].name);
}

optArray = optArray.sort();

$(function() {
  $("#targetNode").autocomplete({
    source: optArray
  });
});

//used to find max thickness of all the nodes, which is used for normalizing later on.
var max_thickness = d3.max(graph.links, function(d) {
  return d.thickness;
});

//used to find min thickness of all the nodes, which is used for normalizing later on.
var min_thickness = d3.min(graph.links, function(d) {
  return d.thickness;
});

var svg1 = d3.select("svg");
var width = +screen.width;
var height = +screen.height - 500;

svg1.attr("width", width).attr("height", height);

var zoom = d3.zoom().on("zoom", zoomed);

function zoomed() {
  svg.attr("transform", d3.event.transform);
}

var svg = svg1
  // .call(
  //   zoom.on("zoom", function() {
  //     svg.attr("transform", d3.event.transform);
  //   })
  // )
  .call(zoom)
  .on("dblclick.zoom", null)
  .append("g");
// Defining the gradient
//used for coloring the nodes

var gradient = svg

  .append("svg:defs")
  .append("svg:linearGradient")
  .attr("id", "gradient")
  .attr("x1", "0%")
  .attr("y1", "0%")
  .attr("x2", "100%")
  .attr("y2", "100%")

  .attr("spreadMethod", "pad");

// Define the gradient colors
gradient
  .append("svg:stop")
  .attr("offset", "0%")
  .attr("stop-color", "#b721ff")
  .attr("stop-opacity", 1);

gradient
  .append("svg:stop")
  .attr("offset", "100%")
  .attr("stop-color", "#21d4fd")
  .attr("stop-opacity", 1);

var linkText = svg
  .selectAll(".gLink")
  .data(graph.links)
  .append("text")
  .attr("font-family", "Arial, Helvetica, sans-serif")
  .attr("x", function(d) {
    if (d.target.x > d.source.x) {
      return d.source.x + (d.target.x - d.source.x) / 2;
    } else {
      return d.target.x + (d.source.x - d.target.x) / 2;
    }
  })
  .attr("y", function(d) {
    if (d.target.y > d.source.y) {
      return d.source.y + (d.target.y - d.source.y) / 2;
    } else {
      return d.target.y + (d.source.y - d.target.y) / 2;
    }
  })
  .attr("fill", "Black")
  .style("font", "normal 12px Arial")
  .attr("dy", ".35em")
  .text(function(d) {
    return d.linkname;
  });

var dragDrop = d3
  .drag()
  .on("start", node => {
    node.fx = node.x;
    node.fy = node.y;
  })
  .on("drag", node => {
    simulation.alphaTarget(1).restart();
    node.fx = d3.event.x;
    node.fy = d3.event.y;
  })
  .on("end", node => {
    if (!d3.event.active) {
      simulation.alphaTarget(0);
    }
    node.fx = null;
    node.fy = null;
  });

var linkForce = d3
  .forceLink(graph.links)
  .id(function(d) {
    return d.name;
  })
  .distance(50);
//.strength(0.5) to specify the pulling strength from each link
// strength from each node

// Saving a reference to node attribute
var nodeForce = d3.forceManyBody().strength(-30);

var simulation = d3
  .forceSimulation(graph.nodes)
  .force("links", linkForce)
  .force("charge", nodeForce)
  .force("center", d3.forceCenter(width / 2, height / 2))
  .on("tick", ticked);

var div = d3
  .select("body")
  .append("div")
  .attr("class", "tooltip")
  .style("opacity", 0);

svg
  .append("defs")
  .append("marker")
  .attrs({
    id: "arrowhead",
    viewBox: "-0 -5 10 10",
    refX: 100,
    refY: 0,
    orient: "auto-start-reverse",
    markerWidth: 3,
    markerHeight: 3
  })
  .append("svg:path")
  .attr("d", "M 0,-5 L 10 ,0 L 0,5")
  .attr("fill", "black");
{
  {
    /* .call(
      d3.zoom().on("zoom", function() {
        console.log(d3.event.transform);
        svg.attr("transform", d3.event.transform);
      })
    ); */
  }
}

var link = svg
  .append("g")
  .selectAll("line_class.line")
  .data(graph.links)
  .enter()
  .append("line")
  .attr("stroke-width", function(d) {
    return Math.max(
      ((d.thickness - min_thickness) / (max_thickness - min_thickness + 0.01)) *
        5,
      3
    );
  })
  .style("stroke", "pink")
  .text("text", function(d) {
    return d.linkname;
  })
  .on("mouseover", function(d) {
    div
      .transition()
      .duration(200)
      .style("opacity", 0.9);
    div
      .html("Name: " + d.linkname)
      .style("left", d3.event.pageX + "px")
      .style("top", d3.event.pageY - 28 + "px")
      .style("width", "auto")
      .style("height", "auto");
  })
  .on("mouseout", function(d) {
    div
      .transition()
      .duration(500)
      .style("opacity", 0);
  })
  .attr("marker-start", "url(#arrowhead)");
// .attr("marker-start", "url(#arrowhead)")
// .attr("marker-mid", "url(#arrowhead)")
// .attr("marker-end", "url(#arrowhead)");

// var line = link

var edgepaths = svg
  .selectAll(".edgepath")
  .data(graph.links)
  .enter()
  .append("path")
  .attr("d", function(d) {
    return (
      "M " +
      d.source.x +
      " " +
      d.source.y +
      " L " +
      d.target.x +
      " " +
      d.target.y
    );
  })
  .attr("class", "edgepath")
  .attr("fill-opacity", 0)
  .attr("stroke-opacity", 0)
  .attr("fill", "blue")
  .attr("stroke", "red")
  .attr("id", function(d, i) {
    return "edgepath" + i;
  })
  .style("pointer-events", "none");

var radius = 4;
var node_data_array = [];
var node_name = "";

var node = svg
  .append("g")
  .selectAll("circle_class.circle")
  .data(graph.nodes)
  .enter()
  .append("rect")
  .attr("width", 40)
  .attr("height", 20)
  .attr("fill", "url(#gradient)")
  .style("transform", "translate(-20px,-10px)")
  .attr("stroke", "purple")
  .attr("id", function(d) {
    node_name = d.name;
    return d.name;
  })
  .on("dblclick", connectedNodes);

var textElements_nodes = svg
  .append("g")
  .selectAll("text")
  .data(graph.nodes)
  .enter()
  .append("text")
  .text(function(d) {
    return d.name;
  })
  .attr("font-size", 15)
  .attr("dx", 15)
  .attr("dy", -5);

node.call(dragDrop);

link.call(updateState1);

var path = document.querySelector("path"),
  totalLength = path.getTotalLength(),
  group = totalLength / 20,
  start;

var arrowheads = d3
  .select("svg")
  .selectAll("use")
  .data(
    d3.range(20).map(function(d) {
      return d * group + 50;
    })
  )
  .enter()
  .append("use")
  .attr("xlink:href", "#arrowhead");

path.style.strokeDasharray = "50," + (group - 50);

requestAnimationFrame(update);

function update(t) {
  if (!start) {
    start = t;
  }

  var offset = (-group * ((t - start) % 900)) / 900;

  path.style.strokeDashoffset = offset;

  arrowheads.attr("transform", function(d) {
    var l = d - offset;

    if (l < 0) {
      l = totalLength + l;
    } else if (l > totalLength) {
      l -= totalLength;
    }

    var p = pointAtLength(l);
    return "translate(" + p + ") rotate( " + angleAtLength(l) + ")";
  });

  //requestAnimationFrame(update);
}

function pointAtLength(l) {
  var xy = path.getPointAtLength(l);
  return [xy.x, xy.y];
}

// Approximate tangent
function angleAtLength(l) {
  var a = pointAtLength(Math.max(l - 0.01, 0)), // this could be slightly negative
    b = pointAtLength(l + 0.01); // browsers cap at total length

  return (Math.atan2(b[1] - a[1], b[0] - a[0]) * 180) / Math.PI;
}

// link.attr("marker-end", "url(#end)");
function updateState1() {
  link.each(function(d) {
    var colors = ["red", "green", "blue"];
    var num = 0;
    if (d.source.name.startsWith("Dummy")) {
      num = 1;
      console.log("Inside 1");
      console.log(
        "Source is ",
        d.source.name,
        " and target is ",
        d.target.name
      );
    } else if (d.target.name.startsWith("Dummy")) {
      num = 2;
      console.log("Inside 2");
      console.log(
        "Source is ",
        d.source.name,
        " and target is ",
        d.target.name
      );
    } else {
      num = 0;
      for (i = 0; i < graph.input_nodes.length; i++) {
        if (graph.input_nodes[i].name == d.source.name) {
          num = 1;
        }
      }
    }

    d3.select(this).style("stroke", function(d) {
      return colors[num];
    });

    {
      {
        /* d3.select(this).attr("marker-end", "url(#arrowhead)"); */
      }
    }
    {
      {
        /* d3.select(this).attr("marker-mid", "url(#arrowhead)"); */
      }
    }
    // .attr("marker-end", "url(#arrowhead)");
  });
}

function pointOnRect(x, y, minX, minY, maxX, maxY, validate) {
  //assert minX <= maxX;
  //assert minY <= maxY;
  if (validate && minX < x && x < maxX && minY < y && y < maxY)
    throw "Point " +
      [x, y] +
      "cannot be inside " +
      "the rectangle: " +
      [minX, minY] +
      " - " +
      [maxX, maxY] +
      ".";
  var midX = (minX + maxX) / 2;
  var midY = (minY + maxY) / 2;
  // if (midX - x == 0) -> m == ±Inf -> minYx/maxYx == x (because value / ±Inf = ±0)
  var m = (midY - y) / (midX - x);

  if (x <= midX) {
    // check "left" side
    var minXy = m * (minX - x) + y;
    if (minY <= minXy && minXy <= maxY) return { x: minX, y: minXy };
  }

  if (x >= midX) {
    // check "right" side
    var maxXy = m * (maxX - x) + y;
    if (minY <= maxXy && maxXy <= maxY) return { x: maxX, y: maxXy };
  }

  if (y <= midY) {
    // check "top" side
    var minYx = (minY - y) / m + x;
    if (minX <= minYx && minYx <= maxX) return { x: minYx, y: minY };
  }

  if (y >= midY) {
    // check "bottom" side
    var maxYx = (maxY - y) / m + x;
    if (minX <= maxYx && maxYx <= maxX) return { x: maxYx, y: maxY };
  }

  // edge case when finding midpoint intersection: m = 0/0 = NaN
  if (x === midX && y === midY) return { x: x, y: y };

  // Should never happen :) If it does, please tell me!
  throw "Cannot find intersection for " +
    [x, y] +
    " inside rectangle " +
    [minX, minY] +
    " - " +
    [maxX, maxY] +
    ".";
}

var label_toggle = 0;
function show_hide_node_labels() {
  if (label_toggle) {
    textElements_nodes.style("visibility", "visible");
  } else {
    textElements_nodes.style("visibility", "hidden");
  }
  label_toggle = !label_toggle;
}

var toggle = 0;

var linkedByIndex = {};
for (i = 0; i < graph.nodes.length; i++) {
  linkedByIndex[i + "," + i] = 1;
}
graph.links.forEach(function(d) {
  linkedByIndex[d.source.index + "," + d.target.index] = 1;
});

//DAT GUI for controls
var gui = new dat.GUI({ width: 300 });

config = {
  linkStrength: 1,
  linkDistance: 180,
  nodeStrength: -30,
  Width: 40,
  Height: 20,
  restart: reset,
  showHideNodeLabels: show_hide_node_labels
};

var linkDistanceChanger = gui
  .add(config, "linkDistance", 0, 400)
  .step(1)
  .name("Link Distance");
linkDistanceChanger.onChange(function(value) {
  linkForce.distance(value);
  simulation.alpha(1).restart();
});

var linkStrengthChanger = gui
  .add(config, "linkStrength", 0, 1)
  .name("Link Strength");
linkStrengthChanger.onChange(function(value) {
  linkForce.strength(value);
  simulation.alpha(1).restart();
});

var nodeStrengthChanger = gui
  .add(config, "nodeStrength", -500, -1)
  .step(1)
  .name("Node Strength");
nodeStrengthChanger.onChange(function(value) {
  nodeForce.strength(value);
  simulation.alpha(1).restart();
});

var widthChanger = gui
  .add(config, "Width", 4, 100)
  .step(1)
  .name("Node Width");
widthChanger.onChange(function(value) {
  node.attr("width", value);
  og_width = value;
  // d3.select("#arrowhead").attrs({
  //   markerWidth: value * 0.4,
  //   markerHeight: value * 0.4
  // });
  simulation.alpha(1).restart();
});

var heightChanger = gui
  .add(config, "Height", 2, 80)
  .step(1)
  .name("Node Height");
heightChanger.onChange(function(value) {
  node.attr("height", value);
  og_height = value;
  // d3.select("#arrowhead").attrs({
  //   markerWidth: value * 0.4,
  //   markerHeight: value * 0.4
  // });
  simulation.alpha(1).restart();
});

gui.add(config, "showHideNodeLabels").name("Show/Hide Node Labels");

gui.add(config, "restart").name("Restart");

for (i = 0; i < gui.__ul.childNodes.length; i++) {
  gui.__ul.childNodes[i].classList += " longtext";
}

function reset() {
  window.location.reload();
}

//This function looks up whether a pair are neighbours
function neighboring(a, b) {
  return linkedByIndex[a.index + "," + b.index];
}

function connectedNodes() {
  if (toggle == 0) {
    //Reduce the opacity of all but the neighbouring nodes
    d = d3.select(this).node().__data__;
    node.style("opacity", function(o) {
      return neighboring(d, o) | neighboring(o, d) ? 1 : 0.1;
    });

    link.style("opacity", function(o) {
      return (d.index == o.source.index) | (d.index == o.target.index)
        ? 1
        : 0.1;
    });

    textElements_nodes.style("opacity", function(o) {
      return neighboring(d, o) | neighboring(o, d) ? 1 : 0.1;
    });

    toggle = 1;
  } else {
    //Put them back to opacity=1
    node.style("opacity", 1);
    link.style("opacity", 1);
    textElements_nodes.style("opacity", 1);
    toggle = 0;
  }
}

function ticked() {
  link
    .attr("x1", function(d) {
      return d.source.x;
    })
    .attr("y1", function(d) {
      return d.source.y;
    })
    .attr("x2", function(d) {
      return d.target.x;
    })
    .attr("y2", function(d) {
      return d.target.y;
    })
    .attr("d", function(d) {
      var inter = pointOnRect(
        d.source.x,
        d.source.y,
        d.target.x - 20,
        d.target.y - 20,
        d.target.x + 40 - 20,
        d.target.y + 20 - 20
      );

      return (
        "M" + d.source.x + "," + d.source.y + "L" + inter.x + "," + inter.y
      );
    });

  node
    .attr("x", function(d) {
      return d.x;
    })
    .attr("y", function(d) {
      return d.y;
    });

  textElements_nodes.attr("x", node => node.x).attr("y", node => node.y);

  edgepaths.attr("d", function(d) {
    var path =
      "M " +
      d.source.x +
      " " +
      d.source.y +
      " L " +
      d.target.x +
      " " +
      d.target.y;
    return path;
  });

  var ticking = false;
}

var pulse = false;

function pulsed(rect) {
  (function repeat() {
    if (pulse) {
      rect
        .transition()
        .duration(200)
        .attr("stroke-width", 0)
        .attr("stroke-opacity", 0)
        .attr("width", config.Width)
        .attr("height", config.Height)
        .transition()
        .duration(200)
        .attr("stroke-width", 0)
        .attr("stroke-opacity", 0.5)
        .attr("width", config.Width * 3)
        .attr("height", config.Height * 3)
        .transition()
        .duration(400)
        .attr("stroke-width", 65)
        .attr("stroke-opacity", 0)
        .attr("width", config.Width)
        .attr("height", config.Height)
        .ease(d3.easeSin)
        .on("end", repeat);
    } else {
      ticking = false;
      rect
        .attr("width", config.Width)
        .attr("height", config.Height)
        .attr("fill", "url(#gradient)")
        .attr("stroke", "purple");
    }
  })();
}

function highlightNode() {
  var userInput = document.getElementById("targetNode");
  var temp = userInput.value;
  var userInputRefined = temp.replace(/[/]/g, "\\/");
  // make userInput work with "/" as they are considered special characters
  // the char "/" is escaped with escape characters.
  theNode = d3.select("#" + userInputRefined);
  const isEmpty = theNode.empty();
  if (isEmpty) {
    document.getElementById("output").innerHTML = "Given node doesn't exist";
  } else {
    document.getElementById("output").innerHTML = "";
  }
  pulse = true;
  if (pulse) {
    pulsed(theNode);
  }

  scalingFactor = 0.5;
  // Create a zoom transform from d3.zoomIdentity
  var transform = d3.zoomIdentity
    .translate(
      screen.width / 2 - scalingFactor * theNode.attr("x"),
      screen.height / 4 - scalingFactor * theNode.attr("y")
    )
    .scale(scalingFactor);
  // Apply the zoom and trigger a zoom event:
  svg1.call(zoom.transform, transform);
}

function minimizeNode() {
  var userInput = document.getElementById("targetNode");
  var temp = userInput.value;
  var userInputRefined = temp.replace(/[/]/g, "\\/");
  // make userInput work with "/" as they are considered special characters
  // the char "/" is escaped with escape characters.
  theNode = d3.select("#" + userInputRefined);
  const isEmpty = theNode.empty();
  if (isEmpty) {
    document.getElementById("output").innerHTML = "Given node doesn't exist";
  } else {
    document.getElementById("output").innerHTML = "";
  }
  pulse = false;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.0.0/d3.min.js"></script>
<!DOCTYPE html>
<html>
  <head>
    <link rel="icon" href="data:;base64,iVBORw0KGgo=" />
    <title>Force Layout Example 9</title>
    <style>
      div.tooltip {
        position: absolute;
        text-align: center;
        width: 60px;
        height: 28px;
        padding: 2px;
        font: 12px sans-serif;
        background: lightsteelblue;
        border: 0px;
        border-radius: 8px;
        pointer-events: none;
      }

      .longtext {
        line-height: 13px;
        height: 40px;
        font-size: 120%;
      }

      rect.zoom-panel {
        cursor: move;
        fill: #fff;
        pointer-events: all;
      }
      
      .bar {
        fill: rgb(70, 180, 70);
      }

      .graph-svg-component {
        background-color: green;
      }

      path {
        fill: none;
        stroke: #d3008c;
        stroke-width: 2px;
      }

      #arrowhead {
        fill: #d3008c;
        stroke: none;
      }
    </style>
  </head>
  <body>
    <svg></svg>

    <div id="content"></div>
    <script src="http://d3js.org/d3.v5.min.js"></script>
    <script src="https://d3js.org/d3-dispatch.v1.min.js"></script>
    <script src="https://d3js.org/d3-selection.v1.min.js"></script>
    <script src="https://d3js.org/d3-drag.v1.min.js"></script>
    <script src="https://d3js.org/d3-selection-multi.v1.min.js"></script>
    <script src="https://d3js.org/d3-transition.v1.min.js"></script>
    <script
      type="text/javascript"
      src="https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.7.6/dat.gui.min.js"
    ></script>

    <link
      href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/themes/base/jquery-ui.css"
      rel="stylesheet"
      type="text/css"
    />
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
    <script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>


  </body>
</html>

Я думаю, что это то, что вы хотите, этот фрагмент кода на основе этого bl.ocks.org фрагмента и он должен быть анимированным если я удалил вызов requestAnimationFrame из функции обновления.

относительно вашей первоначальной проблемы с узлами, скрывающими стрелку, вы можете использовать refX и refY атрибут <marker> для изменения его смещения.

var path = document.querySelector("path"),
    totalLength = path.getTotalLength(),
    group = totalLength / 20,
    start;

var arrowheads = d3.select("svg").selectAll("use")
  .data(d3.range(20).map(function(d){ return d * group + 50; }))
  .enter()
  .append("use")
    .attr("xlink:href", "#arrowhead");

path.style.strokeDasharray = "50," + (group - 50);

requestAnimationFrame(update);

function update(t) {
  if (!start) {
    start = t;
  }

  var offset = -group * ((t - start) % 900) / 900;

  path.style.strokeDashoffset = offset;

  arrowheads.attr("transform",function(d){

    var l = d - offset;

    if (l < 0) {
      l = totalLength + l;
    } else if (l > totalLength) {
      l -= totalLength;
    }

    var p = pointAtLength(l);
    return "translate(" + p + ") rotate( " + angleAtLength(l) + ")";
  });

  //requestAnimationFrame(update);
}

function pointAtLength(l) {

  var xy = path.getPointAtLength(l);
  return [xy.x, xy.y];

}

// Approximate tangent
function angleAtLength(l) {

  var a = pointAtLength(Math.max(l - 0.01,0)), // this could be slightly negative
      b = pointAtLength(l + 0.01); // browsers cap at total length

  return Math.atan2(b[1] - a[1], b[0] - a[0]) * 180 / Math.PI;

}
path {
  fill: none;
  stroke: #d3008c;
  stroke-width: 2px;
}

#arrowhead {
  fill: #d3008c;
  stroke: none;
}
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8" />
</head>
<body>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="960" height="500">
  <path d="M636.5,315c-0.4-18.7,1.9-27.9-5.3-35.9
  	c-22.7-25-107.3-2.8-118.3,35.9c-7,24.4,20.6,37.2,16,71c-4,29.6-30.8,60.7-56.5,61.1c-30.8,0.4-32.9-43.8-81.7-70.2
  	c-50.9-27.6-110.1-12.9-125.2-9.2c-66.1,16.4-82.2,56.9-109.2,47.3c-38-13.6-55.9-112.1-19.8-143.5c39-34,121.2,27.7,148.1-3.8
  	c18-21.1,3.1-74.3-25.2-105.3c-31.1-34.1-70.1-32.4-105.3-76.3c-8.2-10.2-16.9-23.8-15.3-39.7c1.2-11.4,7.5-23.3,15.3-29
  	c33.8-25,101.6,62.6,193.1,59.5c40.1-1.3,38.7-18.5,99.2-38.9c126.2-42.6,242.4-4.9,297.7,13c54.7,17.7,105.4,35,129.8,82.4
  	c13,25.3,22.9,67.7,4.6,87c-11.6,12.3-25.1,5.1-46.6,20.6c-2.8,2-28.9,21.4-32.1,49.6c-3.1,27.4,18.7,35,29,70.2
  	c8.8,30.1,8.5,77.8-18.3,99.2c-32.3,25.8-87,0.6-100-5.3c-69.6-32-67.2-88.4-73.3-109.2z"/>
    <defs>
      <path id="arrowhead" d="M7,0 L-7,-5 L-7,5 Z" />
    </defs>
</svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
...