Search code examples
javascriptimaged3.jsgraphnodes

D3 v7 Force Graph: Images are not showing up


I successfully managed to build a force graph with my data. Now I wanted to display a separate image for each node, but the two approaches I find to accomplish this won't work.

This is the first one:

var node = svg
.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(graph.nodes)
.enter()
.append("circle")
.attr("r", entityRadius)
.call(drag(simulation))
.attr("fill", "url(#bgPattern)");

defs
.append("svg:pattern")
.attr("width", 150)
.attr("height", 150)
.attr("id", "bgPattern")
.append("svg:image")
.data(graph.nodes)
.attr("xlink:href", function (d) {
  return "../assets/images/" + d.compressed;
})
.attr("width", 150)
.attr("height", 150)
.attr("x", 0)
.attr("y", -20);
node.attr("fill", "url(#bgPattern)");

Which works, but only shows one image for some reason. The function only runs once.

And the other one is this:

 node
.append("image")
.attr("xlink:href", function (d) {
  return "../assets/images/" + d.compressed;
})
.attr("width", 150)
.attr("height", 150)
.attr("x", -75)
.attr("y", -75);

Which successfully places an individual image element inside of each circle element, but never renders it.

Images in Circle Elements are not showing up

Does anyone have an idea what is wrong with this code?

Here is the full code with the two approaches:

function blogGraph(graph) {
  const width = window.innerWidth;
  const height = window.innerHeight;

  const sourceRadius = 45;
  const entityRadius = 35;

  var svg = d3
    .select("#networkGraph")
    .append("svg")
    .attr("width", width)
    .attr("height", height)
    .call(
      d3.zoom().on("zoom", function (event) {
        svg.attr("transform", event.transform);
      })
    )
    .append("g");

  var simulation = d3
    .forceSimulation()
    .force(
      "link",
      d3.forceLink().id(function (d) {
        return d.id;
      })
    )
    .force(
      "charge",
      d3.forceManyBody().strength(-2000).theta(0.5).distanceMax(500)
    )
    .force(
      "collision",
      d3.forceCollide().radius(function (d) {
        return d.radius;
      })
    )
    .force("center", d3.forceCenter(width / 2, height / 2));

  var defs = svg.append("defs");

  defs
    .append("radialGradient")
    .attr("id", "entity-gradient")
    .attr("cx", "50%")
    .attr("cy", "50%")
    .attr("r", "50%")
    .selectAll("stop")
    .data([
      { offset: "50%", color: "#ffffff" },
      { offset: "100%", color: "#CCCCCC" },
    ])
    .enter()
    .append("stop")
    .attr("offset", function (d) {
      return d.offset;
    })
    .attr("stop-color", function (d) {
      return d.color;
    });

  defs
    .append("svg:pattern")
    .attr("width", 150)
    .attr("height", 150)
    .attr("id", "bgPattern")
    .append("svg:image")
    .data(graph.nodes)
    .attr("xlink:href", function (d) {
      return "../assets/images/" + d.compressed;
    })
    .attr("width", 150)
    .attr("height", 150)
    .attr("x", 0)
    .attr("y", -20);

  var link = svg
    .append("g")
    .selectAll("line")
    .data(graph.links)
    .enter()
    .append("line");

  link.style("stroke", "#aaa");

  var node = svg
    .append("g")
    .attr("class", "nodes")
    // .selectAll("img")
    // .data(graph.nodes)
    // .enter()
    // .append("img")
    // .attr("xlink:href", function (d) {
    //   return "../assets/images/" + d.compressed;
    // })
    // .attr("width", 150)
    // .attr("height", 150)
    // .attr("x", -150)
    // .attr("y", -150);
    .selectAll("circle")
    .data(graph.nodes)
    .enter()
    .append("circle")
    .attr("r", entityRadius)
    .call(drag(simulation));
  // .attr("fill", "url(#bgPattern)");

  node
    .append("image")
    .attr("xlink:href", function (d) {
      return "../assets/images/" + d.compressed;
    })
    .attr("width", 150)
    .attr("height", 150)
    .attr("x", -75)
    .attr("y", -75);

  node
    .style("fill-opacity", "0.5")
    // .style("fill", "#cccccc")
    .style("stroke", "#424242")
    .style("stroke-width", "1px");

  var label = svg
    .append("g")
    .attr("class", "labels")
    .selectAll("text")
    .data(graph.nodes)
    .enter()
    .append("text")
    .text(function (d) {
      return d.title;
    })
    .attr("class", "label");

  label.style("text-anchor", "middle").style("font-size", function (d) {
    return d.title == "technology"
      ? Math.min(
          2 * entityRadius,
          ((2 * entityRadius - 8) / this.getComputedTextLength()) * 15
        ) + "px"
      : Math.min(
          2 * sourceRadius,
          ((2 * sourceRadius - 8) / this.getComputedTextLength()) * 15
        ) + "px";
  });

  label
    .on("mouseover", function (d) {
      tooltip.html(`${d.title}`);
      return tooltip.style("visibility", "visible");
    })
    .on("mousemove", function (event) {
      return tooltip
        .style("top", event.pageY - 10 + "px")
        .style("left", event.pageX + 10 + "px");
    });

  node
    .on("mouseover", function (d) {
      tooltip.html(`${d.title}`);
      return tooltip.style("visibility", "visible");
    })
    .on("mousemove", function (event) {
      return tooltip
        .style("top", event.pageY - 10 + "px")
        .style("left", event.pageX + 10 + "px");
    })
    .on("mouseout", function () {
      return tooltip.style("visibility", "hidden");
    });

  simulation.nodes(graph.nodes).on("tick", ticked);

  simulation.force("link").links(graph.links);

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

    node
      .attr("cx", function (d) {
        return d.x + 5;
      })
      .attr("cy", function (d) {
        return d.y - 3;
      });

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

  function drag(simulation) {
    function dragstarted(event) {
      if (!event.active) simulation.alphaTarget(0.3).restart();
      event.subject.fx = event.subject.x;
      event.subject.fy = event.subject.y;
    }

    function dragged(event) {
      event.subject.fx = event.x;
      event.subject.fy = event.y;
    }

    function dragended(event) {
      if (!event.active) simulation.alphaTarget(0);
      event.subject.fx = null;
      event.subject.fy = null;
    }

    return d3
      .drag()
      .on("start", dragstarted)
      .on("drag", dragged)
      .on("end", dragended);
  }

  var tooltip = d3
    .select("body")
    .append("div")
    .style("position", "absolute")
    .style("visibility", "hidden")
    .style("color", "white")
    .style("padding", "8px")
    .style("background-color", "#626D71")
    .style("border-radius", "6px")
    .style("text-align", "center")
    .style("width", "auto")
    .text("");
}

var nodesUrl = "https://www.fabianschober.com/json/nodes.json"; 
var linksUrl = "https://www.fabianschober.com/json/links.json";

Promise.all([d3.json(nodesUrl), d3.json(linksUrl)]).then((res) => {
  // console.log(res);
  blogGraph({ nodes: res[0], links: res[1] });
});
<html>
  <head>
    <link rel="stylesheet" href="./styles.css" />
    <script src="https://d3js.org/d3.v7.min.js"></script>
    <script src="./ForceGraph.js"></script>
  </head>

  <body>
    <div id="networkGraph"></div>
  </body>
</html>

PS: The Json Files are getting blocked by CORS, but you may get the idea...


Solution

  • I just found the answer myself. You gotta assign every pattern its own unique ID.

      const nodePatterns = svg
        .selectAll("pattern")
        .data(graph.nodes)
        .enter()
        .append("pattern")
        .attr("id", (d) => `pattern-${d.id}`)
        .attr("width", nodeRadius)
        .attr("height", nodeRadius)
        .append("image")
        .attr("xlink:href", (d) => "../assets/images/" + d.compressed);
    
      var node = svg
        .selectAll("circle")
        .data(graph.nodes)
        .enter()
        .append("circle")
        .attr("r", nodeRadius)
        .style("stroke", "#424242")
        .style("stroke-width", "1px")
        .attr("fill", (d) => `url(#pattern-${d.id})`);