damtypo damtypo - 8 days ago 8
Node.js Question

How do I have a specific d3 node be an image in a force directed graph?

I have made a force directed graph which looks similar to this.note this is a random example from the internet

I would like the Nine Inch Nails node, in the centre, to be an image but the rest of the nodes to just be circles. Following this answer it seemed not too difficult but I just can't get my head around the combonation of svg, defs, patterns and d3.

My code is:

var simulation =
d3.forceSimulation()
.force("charge", d3.forceManyBody().strength(-50))
.force("collide", d3.forceCollide().radius(function (d) { return 15 - d.group}).strength(2).iterations(2))
.force("link", d3.forceLink().id(function(d, i) { return i;}).distance(20).strength(0.9))
.force("center", d3.forceCenter(width/2, height/2))
.force('X', d3.forceX(width/2).strength(0.15))
.force('Y', d3.forceY(height/2).strength(0.15));

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

var defs = svg.append('svg:defs');

defs.append("svg:pattern")
.attr("id", "vit-icon")
.attr("width", 48)
.attr("height", 48)
.attr("patternUnits", "userSpaceOnUse")
.append("svg:image")
.attr("xlink:href", "http://placekitten.com/g/48/48")
.attr("width", 48)
.attr("height", 48)
.attr("x", width/2)
.attr("y", height/2)

var node = svg.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(graph.nodes)
.enter().append("circle")
.attr("id", function(d, i) { return 'c'+i})
.attr("r", radius)
.attr("fill", function(d) {
if(d.group==0) {return "url(#vit-icon)";}
else {return color(d.group); }
})
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));


As I say, it seems straight forward in my mind. Basically what I think I'm trying to do is have the image in the svg pattern, then use an if statement to tell my root node to use the image url rather than fill with colour.

In dev tools I can see inspect the image and it shows the blue area it would take up and the node I want it to be associatated to has the 'url(#vit-icon)' as it's fill attribute. But it is not showing the image or any fill for that node.

What am I doing wrong? Or is this the complete wrong approach?
Thanks.

Answer

In your defs, just change:

.attr("x", width/2)
.attr("y", height/2)

To:

.attr("x", 0)
.attr("y", 0);

Here is a demo:

var nodes = [{
    "id": 1,
}, {
    "id": 2,
}, {
    "id": 3,
}, {
    "id": 4,
}, {
    "id": 5,
}, {
    "id": 6,
}, {
    "id": 7,
}, {
    "id": 8,
}];

var links = [{
    source: 1,
    target: 2
}, {
    source: 1,
    target: 3
}, {
    source: 1,
    target: 4
}, {
    source: 2,
    target: 5
}, {
    source: 2,
    target: 6
}, {
    source: 1,
    target: 7
}, {
    source: 7,
    target: 8
}];

var index = 10;
var svg = d3.select("svg"),
    width = +svg.attr("width"),
    height = +svg.attr("height"),
    node,
    link;

var defs = svg.append('svg:defs');

defs.append("svg:pattern")
    .attr("id", "vit-icon")
    .attr("width", 1)
    .attr("height", 1)
    .append("svg:image")
    .attr("xlink:href", "http://66.media.tumblr.com/avatar_1c725152c551_128.png")
    .attr("width", 48)
    .attr("height", 48)
    .attr("x", 0)
    .attr("y", 0);

var simulation = d3.forceSimulation()
    .force("link", d3.forceLink().id(function(d) {
        return d.id;
    }).distance(100))
    .force("collide", d3.forceCollide(50))
    .force("charge", d3.forceManyBody())
    .force("center", d3.forceCenter(width / 2, height / 2));

link = svg.selectAll(".link")
    .data(links, function(d) {
        return d.target.id;
    })

link = link.enter()
    .append("line")
    .attr("class", "link");

node = svg.selectAll(".node")
    .data(nodes, function(d) {
        return d.id;
    })

node = node.enter()
    .append("g")
    .attr("class", "node")
    .call(d3.drag()
        .on("start", dragstarted)
        .on("drag", dragged)
        .on("end", dragended));

node.append("circle")
    .attr("r", d=> d.id === 1 ? 24 : 14)
    .style("fill", function(d) {
        if (d.id === 1) {
            return "url(#vit-icon)";
        } else {
            return "teal"
        }
    })

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

simulation.force("link")
    .links(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("transform", function(d) {
            return "translate(" + d.x + ", " + d.y + ")";
        });
}

function dragstarted(d) {
    if (!d3.event.active) simulation.alphaTarget(0.3).restart()
}

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

function dragended(d) {
    if (!d3.event.active) simulation.alphaTarget(0);
    d.fx = undefined;
    d.fy = undefined;
}
.link {
  stroke: #aaa;
}

.node {
  pointer-events: all;
  stroke: none;
  stroke-width: 40px;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg width="500" height="300"></svg>